天天看點

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...
注:這篇文章好好看完一定會讓你掌握好指針的本質

C語言最核心的知識就是指針,是以,這一篇的文章主題是「指針與記憶體模型」

說到指針,就不可能脫離開記憶體,學會指針的人分為兩種,一種是不了解記憶體模型,另外一種則是了解。

不了解的對指針的了解就停留在“指針就是變量的位址”這句話,會比較害怕使用指針,特别是各種進階操作。

而了解記憶體模型的則可以把指針用得爐火純青,各種 byte 随意操作,讓人直呼 666。

一、記憶體本質

程式設計的本質其實就是操控資料,資料存放在記憶體中。

是以,如果能更好地了解記憶體的模型,以及 C 如何管理記憶體,就能對程式的工作原理洞若觀火,進而使程式設計能力更上一層樓。

大家真的别認為這是空話,我大一整年都不敢用 C 寫上千行的程式也很抗拒寫 C。

因為一旦上千行,經常出現各種莫名其妙的記憶體錯誤,一不小心就發生了 coredump...... 而且還無從排查,分析不出原因。

相比之下,那時候最喜歡 Java,在 Java 裡随便怎麼寫都不會發生類似的異常,頂多偶爾來個

NullPointerException

,也是比較好排查的。

直到後來對記憶體和指針有了更加深刻的認識,才慢慢會用 C 寫上千行的項目,也很少會再有記憶體問題了。(過于自信

「指針存儲的是變量的記憶體位址」這句話應該任何講 C 語言的書都會提到吧。

是以,要想徹底了解指針,首先要了解 C 語言中變量的存儲本質,也就是記憶體。

1.1 記憶體編址

計算機的記憶體是一塊用于存儲資料的空間,由一系列連續的存儲單元組成,就像下面這樣,

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

每一個單元格都表示 1 個 Bit,一個 bit 在 EE 專業的同學看來就是高低電位,而在 CS 同學看來就是 0、1 兩種狀态。

由于 1 個 bit 隻能表示兩個狀态,是以大佬們規定 8個 bit 為一組,命名為 byte。

并且将 byte 作為記憶體尋址的最小單元,也就是給每個 byte 一個編号,這個編号就叫記憶體的

位址

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

這就相當于,我們給小區裡的每個單元、每個住戶都配置設定一個門牌号: 301、302、403、404、501......

在生活中,我們需要保證門牌号唯一,這樣就能通過門牌号很精準的定位到一家人。

同樣,在計算機中,我們也要保證給每一個 byte 的編号都是唯一的,這樣才能夠保證每個編号都能通路到唯一确定的 byte。

1.2 記憶體位址空間

上面我們說給記憶體中每個 byte 唯一的編号,那麼這個編号的範圍就決定了計算機可尋址記憶體的範圍。

所有編号連起來就叫做記憶體的位址空間,這和大家平時常說的電腦是 32 位還是 64 位有關。

早期 Intel 8086、8088 的 CPU 就是隻支援 16 位位址空間,

寄存器

位址總線

都是 16 位,這意味着最多對 2^16 = 64 Kb 的記憶體編号尋址。

這點記憶體空間顯然不夠用,後來,80286 在 8086 的基礎上将

位址總線

位址寄存器

擴充到了20 位,也被叫做 A20 位址總線。

當時在寫 mini os 的時候,還需要通過 BIOS 中斷去啟動 A20 位址總線的開關。

但是,現在的計算機一般都是 32 位起步了,32 位意味着可尋址的記憶體範圍是 2^32 byte = 4GB。

是以,如果你的電腦是 32 位的,那麼你裝超過 4G 的記憶體條也是無法充分利用起來的。

好了,這就是記憶體和記憶體編址。

1.3 變量的本質

有了記憶體,接下來我們需要考慮,int、double 這些變量是如何存儲在 0、1 單元格的。

在 C 語言中我們會這樣定義變量:

int a = 999;
char c = 'c';


           

當你寫下一個變量定義的時候,實際上是向記憶體申請了一塊空間來存放你的變量。

我們都知道 int 類型占 4 個位元組,并且在計算機中數字都是用補碼(不了解補碼的記得去百度)表示的。

999 換算成補碼就是:0000 0011 1110 0111

這裡有 4 個byte,是以需要四個單元格來存儲:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

有沒有注意到,我們把高位的位元組放在了低位址的地方。

那能不能反過來呢?

當然,這就引出了

大端和小端。

像上面這種将高位位元組放在記憶體低位址的方式叫做

大端

反之,将低位位元組放在記憶體低位址的方式就叫做

小端

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

上面隻說明了 int 型的變量如何存儲在記憶體,而 float、char 等類型實際上也是一樣的,都需要先轉換為補碼。

對于多位元組的變量類型,還需要按照大端或者小端的格式,依次将位元組寫入到記憶體單元。

記住上面這兩張圖,這就是程式設計語言中所有變量的在記憶體中的樣子,不管是 int、char、指針、數組、結構體、對象... 都是這樣放在記憶體的。

二、指針是什麼東西?

2.1 變量放在哪?

上面我說,定義一個變量實際就是向計算機申請了一塊記憶體來存放。

那如果我們要想知道變量到底放在哪了呢?

可以通過運算符&來取得變量實際的位址,這個值就是變量所占記憶體塊的起始位址。

(PS: 實際上這個位址是虛拟位址,并不是真正實體記憶體上的位址

我們可以把這個位址列印出來:

printf("%x", &a);


           

大概會是像這樣的一串數字:0x7ffcad3b8f3c

2.2 指針本質

上面說,我們可以通過&符号擷取變量的記憶體位址,那擷取之後如何來表示這是一個

位址

,而不是一個普通的值呢?

也就是在 C 語言中如何表示位址這個概念呢?

對,就是指針,你可以這樣:

int *pa = &a; 


           

pa 中存儲的就是變量 a 的位址,也叫做指向 a 的指針。

在這裡我想談幾個看起來有點無聊的話題:

為什麼我們需要指針?直接用變量名不行嗎?

當然可以,但是變量名是有局限的。

變量名的本質是什麼?

是變量位址的符号化,變量是為了讓我們程式設計時更加友善,對人友好,可計算機可不認識什麼變量 a,它隻知道位址和指令。

是以當你去檢視 C 語言編譯後的彙編代碼,就會發現變量名消失了,取而代之的是一串串抽象的位址。

你可以認為,編譯器會自動維護一個映射,将我們程式中的變量名轉換為變量所對應的位址,然後再對這個位址去進行讀寫。

也就是有這樣一個映射表存在,将變量名自動轉化為位址:

a  | 0x7ffcad3b8f3c
c  | 0x7ffcad3b8f2c
h  | 0x7ffcad3b8f4c
....



           

說的好!

可是我還是不知道指針存在的必要性,那麼問題來了,看下面代碼:

int func(...) {
  ... 
};

int main() {
 int a;
 func(...);
};


           

假設我有一個需求:

要求在func 函數裡要能夠修改 main 函數裡的變量 a,這下咋整,在 main 函數裡可以直接通過變量名去讀寫 a 所在記憶體。 但是在 func 函數裡是看不見a 的呀。

你說可以通過&取位址符号,将 a 的位址傳遞進去:

int func(int address) {
  ....
};

int main() {
 int a;
 func(&a);
};


           

這樣在func 裡就能擷取到 a 的位址,進行讀寫了。

理論上這是完全沒有問題的,但是問題在于:

編譯器該如何區分一個 int 裡你存的到底是 int 類型的值,還是另外一個變量的位址(即指針)。

這如果完全靠我們程式設計人員去人腦記憶了,會引入複雜性,并且無法通過編譯器檢測一些文法錯誤。

而通過int * 去定義一個指針變量,會非常明确:

這就是另外一個 int 型變量的位址。

編譯器也可以通過類型檢查來排除一些編譯錯誤。

這就是指針存在的必要性。

實際上任何語言都有這個需求,隻不過很多語言為了安全性,給指針戴上了一層枷鎖,将指針包裝成了引用。

可能大家學習的時候都是自然而然的接受指針這個東西,但是還是希望這段啰嗦的解釋對你有一定啟發。

同時,在這裡提點小問題:

既然指針的本質都是變量的記憶體首位址,即一個 int 類型的整數。

那為什麼還要有各種類型呢? 比如 int 指針,float 指針,這個類型影響了指針本身存儲的資訊嗎? 這個類型會在什麼時候發揮作用?

2.3 解引用

上面的問題,就是為了引出指針解引用的。

pa中存儲的是a變量的記憶體位址,那如何通過位址去擷取a的值呢?

這個操作就叫做

解引用

,在 C 語言中通過運算符 *就可以拿到一個指針所指位址的内容了。

比如*pa就能獲得a的值。

我們說指針存儲的是變量記憶體的首位址,那編譯器怎麼知道該從首位址開始取多少個位元組呢?

這就是指針類型發揮作用的時候,編譯器會根據指針的所指元素的類型去判斷應該取多少個位元組。

如果是 int 型的指針,那麼編譯器就會産生提取四個位元組的指令,char 則隻提取一個位元組,以此類推。

下面是指針記憶體示意圖:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

pa 指針首先是一個變量,它本身也占據一塊記憶體,這塊記憶體裡存放的就是 a 變量的首位址。

當解引用的時候,就會從這個首位址連續劃出 4 個 byte,然後按照 int 類型的編碼方式解釋。

2.4 活學活用

别看這個地方很簡單,但卻是深刻了解指針的關鍵。

舉兩個例子來詳細說明:

比如:

float f = 1.0;
short c = *(short*)&f; 


           

你能解釋清楚上面過程,對于 f 變量,在記憶體層面發生了什麼變化嗎?

或者 c 的值是多少?1 ?

實際上,從記憶體層面來說,f 什麼都沒變。

如圖:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

假設這是f 在記憶體中的位模式,這個過程實際上就是把 f 的前兩個 byte 取出來然後按照 short 的方式解釋,然後指派給 c。

詳細過程如下:

  1. &f取得f 的首位址
  2. (short*)&f

上面第二步什麼都沒做,這個表達式隻是說 :

“噢,我認為f這個位址放的是一個 short 類型的變量”

最後當去解引用的時候*(short*)&f時,編譯器會取出前面兩個位元組,并且按照 short 的編碼方式去解釋,并将解釋出的值賦給 c 變量。

這個過程 f的位模式沒有發生任何改變,變的隻是解釋這些位的方式。

當然,這裡最後的值肯定不是 1,至于是什麼,大家可以去真正算一下。

那反過來,這樣呢?

short c = 1;
float f = *(float*)&c;


           

如圖:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

具體過程和上述一樣,但上面肯定不會報錯,這裡卻不一定。

為什麼?

(float*)&c會讓我們從c 的首位址開始取四個位元組,然後按照 float 的編碼方式去解釋。

但是c是 short 類型隻占兩個位元組,那肯定會通路到相鄰後面兩個位元組,這時候就發生了記憶體通路越界。

當然,如果隻是讀,大機率是沒問題的。

但是,有時候需要向這個區域寫入新的值,比如:

*(float*)&c = 1.0;


           

那麼就可能發生 coredump,也就是訪存失敗。

另外,就算是不會 coredump,這種也會破壞這塊記憶體原有的值,因為很可能這是是其它變量的記憶體空間,而我們去覆寫了人家的内容,肯定會導緻隐藏的 bug。

如果你了解了上面這些内容,那麼使用指針一定會更加的自如。

2.6 看個小問題

講到這裡,我們來看一個問題,這是一位群友問的,這是他的需求:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

這是他寫的代碼:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

他把 double 寫進檔案再讀出來,然後發現列印的值對不上。

而關鍵的地方就在于這裡:

char buffer[4];
...
printf("%f %xn", *buffer, *buffer);


           

他可能認為 buffer 是一個指針(準确說是數組),對指針解引用就該拿到裡面的值,而裡面的值他認為是從檔案讀出來的 4 個byte,也就是之前的 float 變量。

注意,這一切都是他認為的,實際上編譯器會認為:

“哦,buffer 是 char類型的指針,那我取第一個位元組出來就好了”。

然後把第一個位元組的值傳遞給了 printf 函數,printf 函數會發現,%f 要求接收的是一個 float 浮點數,那就會自動把第一個位元組的值轉換為一個浮點數列印出來。

這就是整個過程。

錯誤關鍵就是,這個同學誤認為,任何指針解引用都是拿到裡面“我們認為的那個值”,實際上編譯器并不知道,編譯器隻會傻傻的按照指針的類型去解釋。

是以這裡改成:

printf("%f %xn", *(float*)buffer, *(float*)buffer);


           

相當于明确的告訴編譯器:

“buffer指向的這個地方,我放的是一個 float,你給我按照 float 去解釋”

三、 結構體和指針

結構體内包含多個成員,這些成員之間在記憶體中是如何存放的呢?

比如:

struct fraction {
 int num; // 整數部分
 int denom; // 小數部分
};

struct fraction fp;
fp.num = 10;
fp.denom = 2;


           

這是一個定點小數結構體,它在記憶體占 8 個位元組(這裡不考慮記憶體對齊),兩個成員域是這樣存儲的:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

我們把 10 放在了結構體中基位址偏移為 0 的域,2 放在了偏移為 4 的域。

接下來我們做一個正常人永遠不會做的操作:

((fraction*)(&fp.denom))->num = 5; 
((fraction*)(&fp.denom))->denom = 12; 
printf("%dn", fp.denom); // 輸出多少?


           

上面這個究竟會輸出多少呢?自己先思考下噢~

接下來我分析下這個過程發生了什麼:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

首先,&fp.denom表示取結構體 fp 中 denom 域的首位址,然後以這個位址為起始位址取 8 個位元組,并且将它們看做一個 fraction 結構體。

在這個新結構體中,最上面四個位元組變成了 denom 域,而 fp 的 denom 域相當于新結構體的 num 域。

是以:

((fraction*)(&fp.denom))->num = 5

實際上改變的是 fp.denom,而

((fraction*)(&fp.denom))->denom = 12

則是将最上面四個位元組指派為 12。

當然,往那四位元組記憶體寫入值,結果是無法預測的,可能會造成程式崩潰,因為也許那裡恰好存儲着函數調用棧幀的關鍵資訊,也可能那裡沒有寫入權限。

大家初學 C 語言的很多 coredump 錯誤都是類似原因造成的。

是以最後輸出的是 5。

為什麼要講這種看起來莫名其妙的代碼?

就是為了說明結構體的本質其實就是一堆的變量打包放在一起,而通路結構體中的域,就是通過結構體的起始位址,也叫基位址,然後加上域的偏移。

其實,C++、Java 中的對象也是這樣存儲的,無非是他們為了實作某些面向對象的特性,會在資料成員以外,添加一些 Head 資訊,比如C++ 的虛函數表。

實際上,我們是完全可以用 C 語言去模仿的。

這就是為什麼一直說 C 語言是基礎,你真正懂了 C 指針和記憶體,對于其它語言你也會很快的了解其對象模型以及記憶體布局。

四、多級指針

說起多級指針這個東西,我以前大一,最多了解到 2 級,再多真的會把我繞暈,經常也會寫錯代碼。

你要是給我寫個這個:int ******p 能把我搞崩潰,我估計很多同學現在就是這種情況

其實,多級指針也沒那麼複雜,就是指針的指針的指針的指針......非常簡單。

今天就帶大家認識一下多級指針的本質。

首先,我要說一句話,沒有多級指針這種東西,指針就是指針,多級指針隻是為了我們友善表達而取的邏輯概念。

首先看下生活中的快遞櫃:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

這種大家都用過吧,豐巢或者超市儲物櫃都是這樣,每個格子都有一個編号,我們隻需要拿到編号,然後就能找到對應的格子,取出裡面的東西。

這裡的格子就是記憶體單元,編号就是位址,格子裡放的東西就對應存儲在記憶體中的内容。

假設我把一本書,放在了 03 号格子,然後把 03 這個編号告訴你,你就可以根據 03 去取到裡面的書。

那如果我把書放在 05 号格子,然後在 03 号格子隻放一個小紙條,上面寫着:「書放在 05 号」。

你會怎麼做?

當然是打開 03 号格子,然後取出了紙條,根據上面内容去打開 05 号格子得到書。

這裡的 03 号格子就叫指針,因為它裡面放的是指向其它格子的小紙條(位址)而不是具體的書。

明白了嗎?

那我如果把書放在 07 号格子,然後在 05 号格子 放一個紙條:「書放在 07号」,同時在03号格子放一個紙條「書放在 05号」

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

這裡的 03 号格子就叫二級指針,05 号格子就叫指針,而 07 号就是我們平常用的變量。

依次,可類推出 N 級指針。

是以你明白了嗎?同樣的一塊記憶體,如果存放的是别的變量的位址,那麼就叫指針,存放的是實際内容,就叫變量。

int a;
int *pa = &a;
int **ppa = &pa;
int ***pppa = &ppa;


           

上面這段代碼,pa就叫一級指針,也就是平時常說的指針,ppa 就是二級指針。

記憶體示意圖如下:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

不管幾級指針有兩個最核心的東西:

  • 指針本身也是一個變量,需要記憶體去存儲,指針也有自己的位址
  • 指針記憶體存儲的是它所指向變量的位址

這就是我為什麼多級指針是邏輯上的概念,實際上一塊記憶體要麼放實際内容,要麼放其它變量位址,就這麼簡單。

怎麼去解讀int **a這種表達呢?

int ** a 可以把它分為兩部分看,即int* 和 *a,後面 *a 中的*表示 a 是一個指針變量,前面的 int* 表示指針變量a

隻能存放 int* 型變量的位址。

對于二級指針甚至多級指針,我們都可以把它拆成兩部分。

首先不管是多少級的指針變量,它首先是一個指針變量,指針變量就是一個*,其餘的*表示的是這個指針變量隻能存放什麼類型變量的位址。

比如int****a表示指針變量 a 隻能存放int*** 型變量的位址。

五、指針與數組

5.1 一維數組

數組是 C 自帶的基本資料結構,徹底了解數組及其用法是開發高效應用程式的基礎。

數組和指針表示法緊密關聯,在合适的上下文中可以互換。

如下:

int array[10] = {10, 9, 8, 7};
printf("%dn", *array);  //  輸出 10
printf("%dn", array[0]);  // 輸出 10

printf("%dn", array[1]);  // 輸出 9
printf("%dn", *(array+1)); // 輸出 9

int *pa = array;
printf("%dn", *pa);  //  輸出 10
printf("%dn", pa[0]);  // 輸出 10

printf("%dn", pa[1]);  // 輸出 9
printf("%dn", *(pa+1)); // 輸出 9


           

在記憶體中,數組是一塊連續的記憶體空間:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

第 0 個元素的位址稱為數組的首位址,數組名實際就是指向數組首位址,當我們通過array[1]或者*(array + 1) 去通路數組元素的時候。

實際上可以看做 address[offset],address 為起始位址,offset 為偏移量,但是注意這裡的偏移量offset 不是直接和 address相加,而是要乘以數組類型所占位元組數,也就是: address + sizeof(int) * offset。

學過彙編的同學,一定對這種方式不陌生,這是彙編中尋址方式的一種:基址變址尋址。

看完上面的代碼,很多同學可能會認為指針和數組完全一緻,可以互換,這是完全錯誤的。

盡管數組名字有時候可以當做指針來用,但數組的名字不是指針。

最典型的地方就是在 sizeof:

printf
           

第一個将會輸出 40,因為 array包含有 10 個int類型的元素,而第二個在 32 位機器上将會輸出 4,也就是指針的長度。

為什麼會這樣呢?

站在編譯器的角度講,變量名、數組名都是一種符号,它們都是有類型的,它們最終都要和資料綁定起來。

變量名用來指代一份資料,數組名用來指代一組資料(資料集合),它們都是有類型的,以便推斷出所指代的資料的長度。

對,數組也有類型,我們可以将 int、float、char 等了解為基本類型,将數組了解為由基本類型派生得到的稍微複雜一些的類型,

數組的類型由元素的類型和數組的長度共同構成。而 sizeof 就是根據變量的類型來計算長度的,并且計算的過程是在編譯期,而不會在程式運作時。

編譯器在編譯過程中會建立一張專門的表格用來儲存變量名及其對應的資料類型、位址、作用域等資訊。

sizeof 是一個操作符,不是函數,使用 sizeof 時可以從這張表格中查詢到符号的長度。

是以,這裡對數組名使用sizeof可以查詢到數組實際的長度。

pa 僅僅是一個指向 int 類型的指針,編譯器根本不知道它指向的是一個整數,還是一堆整數。

雖然在這裡它指向的是一個數組,但數組也隻是一塊連續的記憶體,沒有開始和結束标志,也沒有額外的資訊來記錄數組到底多長。

是以對 pa 使用 sizeof 隻能求得的是指針變量本身的長度。

也就是說,編譯器并沒有把 pa 和數組關聯起來,pa 僅僅是一個指針變量,不管它指向哪裡,sizeof求得的永遠是它本身所占用的位元組數。

5.2 二維數組

大家不要認為二維數組在記憶體中就是按行、列這樣二維存儲的,實際上,不管二維、三維數組... 都是編譯器的文法糖。

存儲上和一維數組沒有本質差別,舉個例子:

int array[3][3] = {{1, 2,3}, {4, 5,6},{7, 8, 9}};
array[1][1] = 5;


           

或許你以為在記憶體中 array 數組會像一個二維矩陣:

1  2  3
4  5  6
7  8  9


           

可實際上它是這樣的:

1  2  3  4  5  6  7  8  9


           

和一維數組沒有什麼差別,都是一維線性排列。

當我們像 array[1][1]這樣去通路的時候,編譯器會怎麼去計算我們真正所通路元素的位址呢?

為了更加通用化,假設數組定義是這樣的:

int array[n][m]

通路: array[a][b]

那麼被通路元素位址的計算方式就是: array + (m * a + b)

這個就是二維數組在記憶體中的本質,其實和一維數組是一樣的,隻是文法糖包裝成一個二維的樣子。

六、神奇的 void 指針

想必大家一定看到過 void 的這些用法:

void func();
int func1(void);


           

在這些情況下,void 表達的意思就是沒有傳回值或者參數為空。

但是對于 void 型指針卻表示通用指針,可以用來存放任何資料類型的引用。

下面的例子就 是一個 void 指針:

void *ptr;


           

void 指針最大的用處就是在 C 語言中實作泛型程式設計,因為任何指針都可以被賦給 void 指針,void 指針也可以被轉換回原來的指針類型, 并且這個過程指針實際所指向的位址并不會發生變化。

比如:

int num;
int *pi = # 
printf("address of pi: %pn", pi);
void* pv = pi;
pi = (int*) pv; 
printf("address of pi: %pn", pi);


           

這兩次輸出的值都會是一樣:

C語言中CY位什麼時候才能為1_萬字長文帶你從記憶體看指針 | C語言指針完全解析...

平常可能很少會這樣去轉換,但是當你用 C 寫大型軟體或者寫一些通用庫的時候,一定離不開 void 指針,這是 C 泛型的基石,比如 std 庫裡的 sort 函數申明是這樣的:

void qsort(void *base,int nelem,int width,int (*fcmp)(const void *,const void *));


           

所有關于具體元素類型的地方全部用 void 代替。

void 還可以用來實作 C 語言中的多态,這是一個挺好玩的東西。

不過也有需要注意的:

  • 不能對 void 指針解引用

比如:

int num;
void *pv = (void*)#
*pv = 4; // 錯誤


           

為什麼?

因為解引用的本質就是編譯器根據指針所指的類型,然後從指針所指向的記憶體連續取 N 個位元組,然後将這 N 個位元組按照指針的類型去解釋。

比如 int *型指針,那麼這裡 N 就是 4,然後按照 int 的編碼方式去解釋數字。

但是 void,編譯器是不知道它到底指向的是 int、double、或者是一個結構體,是以編譯器沒法對 void 型指針解引用。

七、花式秀技

很多同學認為 C 就隻能面向過程程式設計,實際上利用指針,我們一樣可以在 C 中模拟出對象、繼承、多态等東西。

也可以利用 void 指針實作泛型程式設計,也就是 Java、C++ 中的模闆。

大家如果對 C 實作面向對象、模闆、繼承這些感興趣的話,可以積極一點,點贊,留言~ 呼聲高的話,我就再寫一篇。

實際上也是很有趣的東西,當你知道了如何用 C 去實作這些東西,那你對 C++ 中的對象、Java 中的對象也會了解得更加透徹。

比如為啥有 this 指針,或者 Python 中的 self 究竟是個啥?

關于指針想寫的内容還有很多,這其實也隻算是開了個頭,限于篇幅,以後有機會補齊以下内容:

  • 二維數組和二維指針
  • 數組指針和指針數組
  • 指針運算
  • 函數指針
  • 動态記憶體配置設定: malloc 和 free
  • 堆、棧
  • 函數參數傳遞方式
  • 記憶體洩露
  • 數組退化成指針
  • const 修飾指針
  • ...

絮叨

我其實挺想寫一個系列,大概就是關于記憶體、指針、引用、函數調用、堆棧、面向對象實作機制等等這樣的底層一點的東西。

不知道大家有興趣沒有,有興趣的話,可以評論下~

文章持續更新,可以關注 @程式設計指北 及時閱讀~

繼續閱讀