熱線電話:0755-23712116
郵箱:contact@shuangyi-tech.com
地址:深圳市寶安區(qū)沙井街道后亭茅洲山工業(yè)園工業(yè)大廈全至科技創(chuàng)新園科創(chuàng)大廈2層2A
我們都知道,計(jì)算機(jī)是處理數(shù)據(jù)的設(shè)備,而數(shù)據(jù)的主要存儲(chǔ)位置就是磁盤和內(nèi)存,并且對(duì)于程序員來講,CPU 和內(nèi)存是我們必須了解的兩個(gè)物理結(jié)構(gòu),它是你通向高階程序員很重要的橋梁,那么本篇文章我們就來介紹一下基本的內(nèi)存知識(shí)。
內(nèi)存(Memory)是計(jì)算機(jī)中最重要的部件之一,它是程序與CPU進(jìn)行溝通的橋梁。計(jì)算機(jī)中所有程序的運(yùn)行都是在內(nèi)存中進(jìn)行的,因此內(nèi)存對(duì)計(jì)算機(jī)的影響非常大,內(nèi)存又被稱為主存,其作用是存放 CPU 中的運(yùn)算數(shù)據(jù),以及與硬盤等外部存儲(chǔ)設(shè)備交換的數(shù)據(jù)。只要計(jì)算機(jī)在運(yùn)行中,CPU 就會(huì)把需要運(yùn)算的數(shù)據(jù)調(diào)到主存中進(jìn)行運(yùn)算,當(dāng)運(yùn)算完成后CPU再將結(jié)果傳送出來,主存的運(yùn)行也決定了計(jì)算機(jī)的穩(wěn)定運(yùn)行。
在了解一個(gè)事物之前,你首先得先需要見過它,你才會(huì)有印象,才會(huì)有想要了解的興趣,所以我們首先需要先看一下什么是內(nèi)存以及它的物理結(jié)構(gòu)是怎樣的。
內(nèi)存的內(nèi)部是由各種IC電路組成的,它的種類很龐大,但是其主要分為三種存儲(chǔ)器
內(nèi)存 IC 是一個(gè)完整的結(jié)構(gòu),它內(nèi)部也有電源、地址信號(hào)、數(shù)據(jù)信號(hào)、控制信號(hào)和用于尋址的 IC 引腳來進(jìn)行數(shù)據(jù)的讀寫。下面是一個(gè)虛擬的 IC 引腳示意圖
圖中 VCC 和 GND 表示電源,A0 - A9 是地址信號(hào)的引腳,D0 - D7 表示的是數(shù)據(jù)信號(hào)、RD 和 WR 都是控制信號(hào),我用不同的顏色進(jìn)行了區(qū)分,將電源連接到 VCC 和 GND 后,就可以對(duì)其他引腳傳遞 0 和 1 的信號(hào),大多數(shù)情況下,+5V 表示1,0V 表示 0。
我們都知道內(nèi)存是用來存儲(chǔ)數(shù)據(jù),那么這個(gè)內(nèi)存 IC 中能存儲(chǔ)多少數(shù)據(jù)呢?D0 - D7 表示的是數(shù)據(jù)信號(hào),也就是說,一次可以輸入輸出 8 bit = 1 byte 的數(shù)據(jù)。A0 - A9 是地址信號(hào)共十個(gè),表示可以指定 00000 00000 - 11111 11111 共 2 的 10次方 = 1024個(gè)地址。每個(gè)地址都會(huì)存放 1 byte 的數(shù)據(jù),因此我們可以得出內(nèi)存 IC 的容量就是 1 KB。
如果我們使用的是 512 MB 的內(nèi)存,這就相當(dāng)于是 512000(512 * 1000) 個(gè)內(nèi)存 IC。當(dāng)然,一臺(tái)計(jì)算機(jī)不太可能有這么多個(gè)內(nèi)存 IC ,然而,通常情況下,一個(gè)內(nèi)存 IC 會(huì)有更多的引腳,也就能存儲(chǔ)更多數(shù)據(jù)。
讓我們把關(guān)注點(diǎn)放在內(nèi)存 IC 對(duì)數(shù)據(jù)的讀寫過程上來吧!我們來看一個(gè)對(duì)內(nèi)存IC 進(jìn)行數(shù)據(jù)寫入和讀取的模型
來詳細(xì)描述一下這個(gè)過程,假設(shè)我們要向內(nèi)存 IC 中寫入 1byte 的數(shù)據(jù)的話,它的過程是這樣的:
為了便于記憶,我們把內(nèi)存模型映射成為我們現(xiàn)實(shí)世界的模型,在現(xiàn)實(shí)世界中,內(nèi)存的模型很想我們生活的樓房。在這個(gè)樓房中,1層可以存儲(chǔ)一個(gè)字節(jié)的數(shù)據(jù),樓層號(hào)就是地址,下面是內(nèi)存和樓層整合的模型圖
我們知道,程序中的數(shù)據(jù)不僅只有數(shù)值,還有數(shù)據(jù)類型的概念,從內(nèi)存上來看,就是占用內(nèi)存大?。ㄕ加脴菍訑?shù))的意思。即使物理上強(qiáng)制以 1 個(gè)字節(jié)為單位來逐一讀寫數(shù)據(jù)的內(nèi)存,在程序中,通過指定其數(shù)據(jù)類型,也能實(shí)現(xiàn)以特定字節(jié)數(shù)為單位來進(jìn)行讀寫。
下面是一個(gè)以特定字節(jié)數(shù)為例來讀寫指令字節(jié)的程序的示例
// 定義變量
char a;
short b;
long c;
// 變量賦值
a = 123;
b = 123;
c = 123;
我們分別聲明了三個(gè)變量 a,b,c ,并給每個(gè)變量賦上了相同的 123,這三個(gè)變量表示內(nèi)存的特定區(qū)域。通過變量,即使不指定物理地址,也可以直接完成讀寫操作,操作系統(tǒng)會(huì)自動(dòng)為變量分配內(nèi)存地址。
這三個(gè)變量分別表示 1 個(gè)字節(jié)長度的 char,2 個(gè)字節(jié)長度的 short,表示4 個(gè)字節(jié)的 long。因此,雖然數(shù)據(jù)都表示的是 123,但是其存儲(chǔ)時(shí)所占的內(nèi)存大小是不一樣的。如下所示
這里的 123 都沒有超過每個(gè)類型的最大長度,所以 short 和 long 類型為所占用的其他內(nèi)存空間分配的數(shù)值是0,這里我們采用的是低字節(jié)序列的方式存儲(chǔ)
低字節(jié)序列:將數(shù)據(jù)低位存儲(chǔ)在內(nèi)存低位地址。
高字節(jié)序列:將數(shù)據(jù)的高位存儲(chǔ)在內(nèi)存地位的方式稱為高字節(jié)序列。
指針是 C 語言非常重要的特征,指針也是一種變量,只不過它所表示的不是數(shù)據(jù)的值,而是內(nèi)存的地址。通過使用指針,可以對(duì)任意內(nèi)存地址的數(shù)據(jù)進(jìn)行讀寫。
在了解指針讀寫的過程前,我們先需要了解如何定義一個(gè)指針,和普通的變量不同,在定義指針時(shí),我們通常會(huì)在變量名前加一個(gè) * 號(hào)。例如我們可以用指針定義如下的變量
char *d; // char類型的指針 d 定義
short *e; // short類型的指針 e 定義
long *f; // long類型的指針 f 定義
我們以32位計(jì)算機(jī)為例,32位計(jì)算機(jī)的內(nèi)存地址是 4 字節(jié),在這種情況下,指針的長度也是 32 位。然而,變量 d e f 卻代表了不同的字節(jié)長度,這是為什么呢?
實(shí)際上,這些數(shù)據(jù)表示的是從內(nèi)存中一次讀取的字節(jié)數(shù),比如 d e f 的值都為 100,那么使用 char 類型時(shí)就能夠從內(nèi)存中讀寫 1 byte 的數(shù)據(jù),使用 short 類型就能夠從內(nèi)存讀寫 2 字節(jié)的數(shù)據(jù), 使用 long 就能夠讀寫 4 字節(jié)的數(shù)據(jù),下面是一個(gè)完整的類型字節(jié)表
我們可以用圖來描述一下這個(gè)讀寫過程
數(shù)組是指多個(gè)相同的數(shù)據(jù)類型在內(nèi)存中連續(xù)排列的一種形式。作為數(shù)組元素的各個(gè)數(shù)據(jù)會(huì)通過下標(biāo)編號(hào)來區(qū)分,這個(gè)編號(hào)也叫做索引,如此一來,就可以對(duì)指定索引的元素進(jìn)行讀寫操作。
首先先來認(rèn)識(shí)一下數(shù)組,我們還是用 char、short、long 三種元素來定義數(shù)組,數(shù)組的元素用[value] 擴(kuò)起來,里面的值代表的是數(shù)組的長度,就像下面的定義
char g[100];
short h[100];
long i[100];
數(shù)組定義的數(shù)據(jù)類型,也表示一次能夠讀寫的內(nèi)存大小,char 、short 、long 分別以 1 、2 、4 個(gè)字節(jié)為例進(jìn)行內(nèi)存的讀寫。
數(shù)組是內(nèi)存的實(shí)現(xiàn),數(shù)組和內(nèi)存的物理結(jié)構(gòu)完全一致,尤其是在讀寫1個(gè)字節(jié)的時(shí)候,當(dāng)字節(jié)數(shù)超過 1 時(shí),只能通過逐個(gè)字節(jié)來讀取,下面是內(nèi)存的讀寫過程
數(shù)組是我們學(xué)習(xí)的第一個(gè)數(shù)據(jù)結(jié)構(gòu),我們都知道數(shù)組的檢索效率是比較快的,至于數(shù)組的檢索效率為什么這么快并不是我們這篇文章討論的重點(diǎn)。
我們上面提到數(shù)組是內(nèi)存的一種實(shí)現(xiàn),使用數(shù)組能夠使編程更加高效,下面我們就來認(rèn)識(shí)一下其他數(shù)據(jù)結(jié)構(gòu),通過這些數(shù)據(jù)結(jié)構(gòu)也可以操作內(nèi)存的讀寫。
棧(stack)是一種很重要的數(shù)據(jù)結(jié)構(gòu),棧采用 LIFO(Last In First Out)即后入先出的方式對(duì)內(nèi)存進(jìn)行操作。它就像一個(gè)大的收納箱,你可以往里面放相同類型的東西,比如書,最先放進(jìn)收納箱的書在最下面,最后放進(jìn)收納箱的書在最上面,如果你想拿書的話, 必須從最上面開始取,否則是無法取出最下面的書籍的。
棧的數(shù)據(jù)結(jié)構(gòu)就是這樣,你把書籍壓入收納箱的操作叫做壓入(push),你把書籍從收納箱取出的操作叫做彈出(pop),它的模型圖大概是這樣
入棧相當(dāng)于是增加操作,出棧相當(dāng)于是刪除操作,只不過叫法不一樣。棧和內(nèi)存不同,它不需要指定元素的地址。它的大概使用如下
// 壓入數(shù)據(jù)
Push(123);
Push(456);
Push(789);
// 彈出數(shù)據(jù)
j = Pop();
k = Pop();
l = Pop();
在棧中,LIFO 方式表示棧的數(shù)組中所保存的最后面的數(shù)據(jù)(Last In)會(huì)被最先讀取出來(First On)。
隊(duì)列和棧很相似但又不同,相同之處在于隊(duì)列也不需要指定元素的地址,不同之處在于隊(duì)列是一種 先入先出(First In First Out) 的數(shù)據(jù)結(jié)構(gòu)。隊(duì)列在我們生活中的使用很像是我們?nèi)ゾ皡^(qū)排隊(duì)買票一樣,第一個(gè)排隊(duì)的人最先買到票,以此類推,俗話說: 先到先得。它的使用如下
// 往隊(duì)列中寫入數(shù)據(jù)
EnQueue(123);
EnQueue(456);
EnQueue(789);
// 從隊(duì)列中讀出數(shù)據(jù)
m = DeQueue();
n = DeQueue();
o = DeQueue();
向隊(duì)列中寫入數(shù)據(jù)稱為 EnQueue()入列,從隊(duì)列中讀出數(shù)據(jù)稱為DeQueue()。
與棧相對(duì),F(xiàn)IFO 的方式表示隊(duì)列中最先所保存的數(shù)據(jù)會(huì)優(yōu)先被讀取出來。
隊(duì)列的實(shí)現(xiàn)一般有兩種:順序隊(duì)列 和 循環(huán)隊(duì)列,我們上面的事例使用的是順序隊(duì)列,那么下面我們看一下循環(huán)隊(duì)列的實(shí)現(xiàn)方式
環(huán)形緩沖區(qū)
循環(huán)隊(duì)列一般是以環(huán)狀緩沖區(qū)(ring buffer)的方式實(shí)現(xiàn)的,它是一種用于表示一個(gè)固定尺寸、頭尾相連的緩沖區(qū)的數(shù)據(jù)結(jié)構(gòu),適合緩存數(shù)據(jù)流。假如我們要用 6 個(gè)元素的數(shù)組來實(shí)現(xiàn)一個(gè)環(huán)形緩沖區(qū),這時(shí)可以從起始位置開始有序的存儲(chǔ)數(shù)據(jù),然后再按照存儲(chǔ)時(shí)的順序把數(shù)據(jù)讀出。在數(shù)組的末尾寫入數(shù)據(jù)后,后一個(gè)數(shù)據(jù)就會(huì)從緩沖區(qū)的頭開始寫。這樣,數(shù)組的末尾和開頭就連接了起來。
下面我們來介紹一下鏈表和 二叉樹,它們都是可以不用考慮索引的順序就可以對(duì)元素進(jìn)行讀寫的方式。通過使用鏈表,可以高效的對(duì)數(shù)據(jù)元素進(jìn)行添加 和 刪除操作。而通過使用二叉樹,則可以更高效的對(duì)數(shù)據(jù)進(jìn)行檢索。
在實(shí)現(xiàn)數(shù)組的基礎(chǔ)上,除了數(shù)據(jù)的值之外,通過為其附帶上下一個(gè)元素的索引,即可實(shí)現(xiàn)鏈表。數(shù)據(jù)的值和下一個(gè)元素的地址(索引)就構(gòu)成了一個(gè)鏈表元素,如下所示
對(duì)鏈表的添加和刪除都是非常高效的,我們來敘述一下這個(gè)添加和刪除的過程,假如我們要?jiǎng)h除地址為 p[2] 的元素,鏈表該如何變化呢?
我們可以看到,刪除地址為 p[2] 的元素后,直接將鏈表剔除,并把 p[2] 前一個(gè)位置的元素 p[1] 的指針域指向 p[2] 下一個(gè)鏈表元素的數(shù)據(jù)區(qū)即可。
那么對(duì)于新添加進(jìn)來的鏈表,需要確定插入位置,比如要在 p[2] 和 p[3] 之間插入地址為 p[6] 的元素,需要將 p[6] 的前一個(gè)位置 p[2] 的指針域改為 p[6] 的地址,然后將 p[6] 的指針域改為 p[3] 的地址即可。
鏈表的添加不涉及到數(shù)據(jù)的移動(dòng),所以鏈表的添加和刪除很快,而數(shù)組的添加設(shè)計(jì)到數(shù)據(jù)的移動(dòng),所以比較慢,通常情況下,使用數(shù)組來檢索數(shù)據(jù),使用鏈表來進(jìn)行添加和刪除操作。
二叉樹也是一種檢索效率非常高的數(shù)據(jù)結(jié)構(gòu),二叉樹是指在鏈表的基礎(chǔ)上往數(shù)組追加元素時(shí),考慮到數(shù)組的大小關(guān)系,將其分成左右兩個(gè)方向的表現(xiàn)形式。假如我們把 50 這個(gè)值保存到了數(shù)組中,那么,如果接下來要進(jìn)行值寫入的話,就需要和50比較,確定誰大誰小,比50數(shù)值大的放右邊,小的放左邊,下圖是二叉樹的比較示例
二叉樹是由鏈表發(fā)展而來,因此二叉樹在追加和刪除元素方面也是同樣有效的。
這一切的演變都是以內(nèi)存為基礎(chǔ)的。