程式設計師需要了解的硬核知識之內存

程序員cxuan 發佈 2021-08-03T08:50:56.414640+00:00

A0 - A9 是地址信號共十個,表示可以指定 00000 00000 - 11111 11111 共 2 的 10次方 = 1024個地址。

我把自己以往的文章匯總成為了 Github ,歡迎各位大佬 star
https://github.com/crisxuan/bestJavaer
已提交此篇文章


我們都知道,計算機是處理數據的設備,而數據的主要存儲位置就是磁碟和內存,並且對於程式設計師來講,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 字節的數據,下面是一個完整的類型字節表



我們可以用圖來描述一下這個讀寫過程

數組是內存的實現

數組是指多個相同的數據類型在內存中連續排列的一種形式。作為數組元素的各個數據會通過下標編號來區分,這個編號也叫做索引,如此一來,就可以對指定索引的元素進行讀寫操作。

首先先來認識一下數組,我們還是用 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數值大的放右邊,小的放左邊,下圖是二叉樹的比較示例



二叉樹是由鍊表發展而來,因此二叉樹在追加和刪除元素方面也是同樣有效的。

這一切的演變都是以內存為基礎的。

另外,cxuan 肝了六本 PDF,公號回復 cxuan ,領取作者全部 PDF


關鍵字: