我們都知道,計算機是處理數據的設備,而數據的主要存儲位置就是磁盤
和內存
,並且對於程序員來講,CPU 和內存是我們必須了解的兩個物理結構,它是你通向高階程序員很重要的橋梁,那么本篇文章我們就來介紹一下基本的內存知識。
什么是內存
內存(Memory)是計算機中最重要的部件之一,它是程序與CPU進行溝通的橋梁。計算機中所有程序的運行都是在內存中進行的,因此內存對計算機的影響非常大,內存又被稱為主存
,其作用是存放 CPU 中的運算數據,以及與硬盤等外部存儲設備交換的數據。只要計算機在運行中,CPU 就會把需要運算的數據調到主存中進行運算,當運算完成后CPU再將結果傳送出來,主存的運行也決定了計算機的穩定運行。
內存的物理結構
在了解一個事物之前,你首先得先需要見
過它,你才會有印象,才會有想要了解的興趣,所以我們首先需要先看一下什么是內存以及它的物理結構是怎樣的。
內存的內部是由各種IC電路組成的,它的種類很龐大,但是其主要分為三種存儲器
- 隨機存儲器(RAM): 內存中最重要的一種,表示既可以從中讀取數據,也可以寫入數據。當機器關閉時,內存中的信息會
丟失
。 - 只讀存儲器(ROM):ROM 一般只能用於數據的讀取,不能寫入數據,但是當機器停電時,這些數據不會丟失。
- 高速緩存(Cache):Cache 也是我們經常見到的,它分為一級緩存(L1 Cache)、二級緩存(L2 Cache)、三級緩存(L3 Cache)這些數據,它位於內存和 CPU 之間,是一個讀寫速度比內存
更快
的存儲器。當 CPU 向內存寫入數據時,這些數據也會被寫入高速緩存中。當 CPU 需要讀取數據時,會直接從高速緩存中直接讀取,當然,如需要的數據在Cache中沒有,CPU會再去讀取內存中的數據。
內存 IC 是一個完整的結構,它內部也有電源、地址信號、數據信號、控制信號和用於尋址的 IC 引腳來進行數據的讀寫。下面是一個虛擬的 IC 引腳示意圖
圖中 VCC 和 GND 表示電源,A0 - A9 是地址信號的引腳,D0 - D7 表示的是數據信號、RD 和 WR 都是控制信號,我用不同的顏色進行了區分,將電源連接到 VCC 和 GND 后,就可以對其他引腳傳遞 0 和 1 的信號,大多數情況下,+5V 表示1,0V 表示 0。
我們都知道內存是用來存儲數據,那么這個內存 IC 中能存儲多少數據呢?D0 - D7 表示的是數據信號,也就是說,一次可以輸入輸出 8 bit = 1 byte 的數據。A0 - A9 是地址信號共十個,表示可以指定 00000 00000 - 11111 11111 共 2 的 10次方 = 1024個地址
。每個地址都會存放 1 byte 的數據,因此我們可以得出內存 IC 的容量就是 1 KB。
如果我們使用的是 512 MB 的內存,這就相當於是 512000(512 * 1000) 個內存 IC。當然,一台計算機不太可能有這么多個內存 IC ,然而,通常情況下,一個內存 IC 會有更多的引腳,也就能存儲更多數據。
內存的讀寫過程
讓我們把關注點放在內存 IC 對數據的讀寫過程上來吧!我們來看一個對內存IC 進行數據寫入和讀取的模型
來詳細描述一下這個過程,假設我們要向內存 IC 中寫入 1byte 的數據的話,它的過程是這樣的:
- 首先給 VCC 接通 +5V 的電源,給 GND 接通 0V 的電源,使用
A0 - A9
來指定數據的存儲場所,然后再把數據的值輸入給D0 - D7
的數據信號,並把WR(write)
的值置為 1,執行完這些操作后,即可以向內存 IC 寫入數據 - 讀出數據時,只需要通過 A0 - A9 的地址信號指定數據的存儲場所,然后再將 RD 的值置為 1 即可。
- 圖中的 RD 和 WR 又被稱為控制信號。其中當WR 和 RD 都為 0 時,無法進行寫入和讀取操作。
內存的現實模型
為了便於記憶,我們把內存模型映射成為我們現實世界的模型,在現實世界中,內存的模型很想我們生活的樓房。在這個樓房中,1層可以存儲一個字節的數據,樓層號就是地址
,下面是內存和樓層整合的模型圖
我們知道,程序中的數據不僅只有數值,還有數據類型
的概念,從內存上來看,就是占用內存大小(占用樓層數)的意思。即使物理上強制以 1 個字節為單位來逐一讀寫數據的內存,在程序中,通過指定其數據類型,也能實現以特定字節數為單位來進行讀寫。
下面是一個以特定字節數為例來讀寫指令字節的程序的示例
// 定義變量
char a;
short b;
long c;
// 變量賦值
a = 123;
b = 123;
c = 123;
我們分別聲明了三個變量 a,b,c ,並給每個變量賦上了相同的 123,這三個變量表示內存的特定區域。通過變量,即使不指定物理地址,也可以直接完成讀寫操作,操作系統會自動為變量分配內存地址。
這三個變量分別表示 1 個字節長度的 char,2 個字節長度的 short,表示4 個字節的 long。因此,雖然數據都表示的是 123,但是其存儲時所占的內存大小是不一樣的。如下所示
這里的 123 都沒有超過每個類型的最大長度,所以 short 和 long 類型為所占用的其他內存空間分配的數值是0,這里我們采用的是低字節序列
的方式存儲
低字節序列:將數據低位存儲在內存低位地址。
高字節序列:將數據的高位存儲在內存地位的方式稱為高字節序列。
內存的使用
指針
指針是 C 語言非常重要的特征,指針也是一種變量,只不過它所表示的不是數據的值,而是內存的地址。通過使用指針,可以對任意內存地址的數據進行讀寫。
在了解指針讀寫的過程前,我們先需要了解如何定義一個指針,和普通的變量不同,在定義指針時,我們通常會在變量名前加一個 *
號。例如我們可以用指針定義如下的變量
char *d; // char類型的指針 d 定義
short *e; // short類型的指針 e 定義
long *f; // long類型的指針 f 定義
我們以32
位計算機為例,32位計算機的內存地址是 4 字節,在這種情況下,指針的長度也是 32 位。然而,變量 d e f 卻代表了不同的字節長度,這是為什么呢?
實際上,這些數據表示的是從內存中一次讀取的字節數,比如 d e f 的值都為 100,那么使用 char 類型時就能夠從內存中讀寫 1 byte 的數據,使用 short 類型就能夠從內存讀寫 2 字節的數據, 使用 long 就能夠讀寫 4 字節的數據,下面是一個完整的類型字節表
類型 | 32位 | 64位 |
---|---|---|
char | 1 | 1 |
short int | 2 | 2 |
int | 4 | 4 |
unsigned int | 4 | 4 |
float | 4 | 4 |
double | 8 | 8 |
long | 4 | 8 |
long long | 8 | 8 |
unsigned long | 4 | 8 |
我們可以用圖來描述一下這個讀寫過程
數組是內存的實現
數組是指多個相同
的數據類型在內存中連續排列的一種形式。作為數組元素的各個數據會通過下標編號
來區分,這個編號也叫做索引
,如此一來,就可以對指定索引的元素進行讀寫操作。
首先先來認識一下數組,我們還是用 char、short、long 三種元素來定義數組,數組的元素用[value]
擴起來,里面的值代表的是數組的長度,就像下面的定義
char g[100];
short h[100];
long i[100];
數組定義的數據類型,也表示一次能夠讀寫的內存大小,char 、short 、long 分別以 1 、2 、4 個字節為例進行內存的讀寫。
數組是內存的實現,數組和內存的物理結構完全一致,尤其是在讀寫1個字節的時候,當字節數超過 1 時,只能通過逐個字節來讀取,下面是內存的讀寫過程
數組是我們學習的第一個數據結構,我們都知道數組的檢索效率是比較快的,至於數組的檢索效率為什么這么快並不是我們這篇文章討論的重點。
棧和隊列
我們上面提到數組是內存的一種實現,使用數組能夠使編程更加高效,下面我們就來認識一下其他數據結構,通過這些數據結構也可以操作內存的讀寫。
棧
棧(stack)是一種很重要的數據結構,棧采用 LIFO(Last In First Out)即后入先出
的方式對內存進行操作。它就像一個大的收納箱,你可以往里面放相同類型的東西,比如書,最先放進收納箱的書在最下面,最后放進收納箱的書在最上面,如果你想拿書的話, 必須從最上面開始取,否則是無法取出最下面的書籍的。
棧的數據結構就是這樣,你把書籍壓入收納箱的操作叫做壓入(push)
,你把書籍從收納箱取出的操作叫做彈出(pop)
,它的模型圖大概是這樣
入棧相當於是增加操作,出棧相當於是刪除操作,只不過叫法不一樣。棧和內存不同,它不需要指定元素的地址。它的大概使用如下
// 壓入數據
Push(123);
Push(456);
Push(789);
// 彈出數據
j = Pop();
k = Pop();
l = Pop();
在棧中,LIFO 方式表示棧的數組中所保存的最后面的數據(Last In)會被最先讀取出來(First On)。
隊列
隊列
和棧很相似但又不同,相同之處在於隊列也不需要指定元素的地址,不同之處在於隊列是一種 先入先出(First In First Out)
的數據結構。隊列在我們生活中的使用很像是我們去景區排隊買票一樣,第一個排隊的人最先買到票,以此類推,俗話說: 先到先得。它的使用如下
// 往隊列中寫入數據
EnQueue(123);
EnQueue(456);
EnQueue(789);
// 從隊列中讀出數據
m = DeQueue();
n = DeQueue();
o = DeQueue();
向隊列中寫入數據稱為 EnQueue()
入列,從隊列中讀出數據稱為DeQueue()
。
與棧相對,FIFO 的方式表示隊列中最先所保存的數據會優先被讀取出來。
隊列的實現一般有兩種:順序隊列
和 循環隊列
,我們上面的事例使用的是順序隊列,那么下面我們看一下循環隊列的實現方式
環形緩沖區
循環隊列一般是以環狀緩沖區(ring buffer)
的方式實現的,它是一種用於表示一個固定尺寸、頭尾相連的緩沖區的數據結構,適合緩存數據流。假如我們要用 6 個元素的數組來實現一個環形緩沖區,這時可以從起始位置開始有序的存儲數據,然后再按照存儲時的順序把數據讀出。在數組的末尾寫入數據后,后一個數據就會從緩沖區的頭開始寫。這樣,數組的末尾和開頭就連接了起來。
鏈表
下面我們來介紹一下鏈表
和 二叉樹
,它們都是可以不用考慮索引的順序就可以對元素進行讀寫的方式。通過使用鏈表,可以高效的對數據元素進行添加
和 刪除
操作。而通過使用二叉樹,則可以更高效的對數據進行檢索
。
在實現數組的基礎上,除了數據的值之外,通過為其附帶上下一個元素的索引,即可實現鏈表
。數據的值和下一個元素的地址(索引)就構成了一個鏈表元素,如下所示
對鏈表的添加和刪除都是非常高效的,我們來敘述一下這個添加和刪除的過程,假如我們要刪除地址為 p[2]
的元素,鏈表該如何變化呢?
我們可以看到,刪除地址為 p[2] 的元素后,直接將鏈表剔除,並把 p[2] 前一個位置的元素 p[1] 的指針域
指向 p[2] 下一個鏈表元素的數據區即可。
那么對於新添加進來的鏈表,需要確定插入位置
,比如要在 p[2] 和 p[3] 之間插入地址為 p[6]
的元素,需要將 p[6] 的前一個位置 p[2] 的指針域改為 p[6] 的地址,然后將 p[6] 的指針域改為 p[3] 的地址即可。
鏈表的添加不涉及到數據的移動
,所以鏈表的添加和刪除很快,而數組的添加設計到數據的移動,所以比較慢,通常情況下,使用數組來檢索數據,使用鏈表來進行添加和刪除操作。
二叉樹
二叉樹
也是一種檢索效率非常高的數據結構,二叉樹是指在鏈表的基礎上往數組追加元素時,考慮到數組的大小關系,將其分成左右兩個方向的表現形式。假如我們把 50 這個值保存到了數組中,那么,如果接下來要進行值寫入的話,就需要和50比較,確定誰大誰小,比50數值大的放右邊,小的放左邊,下圖是二叉樹的比較示例
二叉樹是由鏈表發展而來,因此二叉樹在追加和刪除元素方面也是同樣有效的。
這一切的演變都是以內存為基礎的。
下面為自己做個宣傳,歡迎關注公眾號 Java建設者,號主是Java技術棧,熱愛技術,喜歡閱讀,熱衷於分享和總結,希望能把每一篇好文章分享給成長道路上的你。公號回復002,有你想要的資源。
文章參考:
https://www.computerhope.com/jargon/m/memory.htm
https://baike.baidu.com/item/隊列/14580481?fr=aladdin
https://baike.baidu.com/item/棧/12808149?fr=aladdin
https://baike.baidu.com/item/環形緩沖器/22701730?fr=aladdin
《程序是怎樣跑起來的》第四章