本篇文章來自小北學長的公眾號,僅做學習使用,部分內容做了適當理解性修改和新增了博主的個人經歷。
注:這篇文章好好看完一定會讓你掌握好指標的本質!
看到標題有沒有想到什麼?
是的,這一篇的文章主題是「指標與記憶體模型」
說到指標,就不可能脫離開記憶體,學會指標的人分為兩種,一種是不瞭解記憶體模型,另外一種則是瞭解。
不瞭解的對指標的理解就停留在“指標就是變數的地址”這句話,會比較害怕使用指標,特別是各種高階操作。
而瞭解記憶體模型的則可以把指標用得爐火純青,各種 byte 隨意操作,讓人直呼 666。
這篇看完,相信你會對指標有一個新的認識,坐等打臉?
一、記憶體本質
程式設計的本質其實就是更好的操控資料,而我們的資料是存放在記憶體中。
因此,如果能更好地理解記憶體的模型,以及 C 如何管理記憶體,就能對程式的工作原理洞若觀火,從而使程式設計能力更上一層樓。
大家真的別認為這是空話,我大一整年都不敢用 C 寫上千行的程式也很抗拒寫 C。
只有到最後課程要求用C寫一個地鐵管理系統和自主學習寫紅黑樹完整的寫了超過千行的程式碼
因為一旦上千行,經常出現各種莫名其妙的記憶體錯誤,一不小心就發生了 coredump...... 而且還無從排查,分析不出原因。
直到後來對記憶體和指標有了更加深刻的認識,才慢慢會用 C\C++ 寫上千行的專案,也很少會再有記憶體問題了。
(過於自信
「指標儲存的是變數的記憶體地址」這句話應該任何講 C 語言的書都會提到吧。
所以,要想徹底理解指標,首先要理解 C 語言中變數的儲存本質,也就是記憶體。
1.1 記憶體編址
計算機的記憶體是一塊用於儲存資料的空間,由一系列連續的儲存單元組成,就像下面這樣,
每一個單元格都表示 1 個 Bit,一個 bit 在 EE 專業的同學看來就是高低電位,而在 CS 同學看來就是 0、1 兩種狀態。
由於 1 個 bit 只能表示兩個狀態,所以大佬們規定 8個 bit 為一組,命名為 byte。
並且將 byte 作為記憶體定址的最小單元,也就是給每個 byte 一個編號,這個編號就叫記憶體的地址。
這就相當於,我們給小區裡的每個單元、每個住戶都分配一個門牌號: 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
c 這裡有 4 個byte,所以需要四個單元格來儲存:
有沒有注意到,我們把高位的位元組放在了低地址的地方。
那能不能反過來呢?
當然,這就引出了大端和小端。
像上面這種將高位位元組放在記憶體低地址的方式叫做大端
反之,將低位位元組放在記憶體低地址的方式就叫做小端:
上面只說明瞭 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 則只提取一個位元組,以此類推。
下面是指標記憶體示意圖:
pa
指標首先是一個變數,它本身也佔據一塊記憶體,這塊記憶體裡存放的就是 a
變數的首地址。
當解引用的時候,就會從這個首地址連續劃出 4 個 byte,然後按照 int 型別的編碼方式解釋。
2.4 活學活用
別看這個地方很簡單,但卻是深刻理解指標的關鍵。
舉兩個例子來詳細說明:
比如:
float f = 1.0;
short c = *(short*)&f;
你能解釋清楚上面過程,對於 f
變數,在記憶體層面發生了什麼變化嗎?
或者 c
的值是多少?1 ?
實際上,從記憶體層面來說,f
什麼都沒變。
如圖:
假設這是f
在記憶體中的位模式,這個過程實際上就是把 f
的前兩個 byte 取出來然後按照 short 的方式解釋,然後賦值給 c
。
詳細過程如下:
&f
取得f
的首地址(short*)&f
上面第二步什麼都沒做,這個表示式只是說 :
“噢,我認為f
這個地址放的是一個 short 型別的變數”
最後當去解引用的時候*(short*)&f
時,編譯器會取出前面兩個位元組,並且按照 short 的編碼方式去解釋,並將解釋出的值賦給 c
變數。
這個過程 f
的位模式沒有發生任何改變,變的只是解釋這些位的方式。
當然,這裡最後的值肯定不是 1,至於是什麼,大家可以去真正算一下。
那反過來,這樣呢?
short c = 1;
float f = *(float*)&c;
如圖:
具體過程和上述一樣,但上面肯定不會報錯,這裡卻不一定。
為什麼?
(float*)&c
會讓我們從c
的首地址開始取四個位元組,然後按照 float 的編碼方式去解釋。
但是c
是 short 型別只佔兩個位元組,那肯定會訪問到相鄰後面兩個位元組,這時候就發生了記憶體訪問越界。
當然,如果只是讀,大概率是沒問題的。
但是,有時候需要向這個區域寫入新的值,比如:
*(float*)&c = 1.0;
那麼就可能發生 coredump,也就是訪存失敗。
另外,就算是不會 coredump,這種也會破壞這塊記憶體原有的值,因為很可能這是是其它變數的記憶體空間,而我們去覆蓋了人家的內容,肯定會導致隱藏的 bug。
如果你理解了上面這些內容,那麼使用指標一定會更加的自如。
2.6 看個小問題
講到這裡,我們來看一個問題,這是一位C語言交流群的群友問的,這是他的需求:
這是他寫的程式碼:
他把 double 寫進檔案再讀出來,然後發現列印的值對不上。
而關鍵的地方就在於這裡:
char buffer[4];
...
printf("%f %x\n", *buffer, *buffer);
他可能認為 buffer
是一個指標(準確說是陣列),對指標解引用就該拿到裡面的值,而裡面的值他認為是從檔案讀出來的 4 個byte,也就是之前的 float 變數。
注意,這一切都是他認為的,實際上編譯器會認為:
“哦,buffer
是 char型別的指標,那我取第一個位元組出來就好了”。
然後把第一個位元組的值傳遞給了 printf 函式,printf 函式會發現,%f
要求接收的是一個 float 浮點數,那就會自動把第一個位元組的值轉換為一個浮點數列印出來。
這就是整個過程。
錯誤關鍵就是,這個同學誤認為,任何指標解引用都是拿到裡面“我們認為的那個值”,實際上編譯器並不知道,編譯器只會傻傻的按照指標的型別去解釋。
所以這裡改成:
printf("%f %x\n", *(float*)buffer, *(float*)buffer);
相當於明確的告訴編譯器:
“buffer
指向的這個地方,我放的是一個 float,你給我按照 float 去解釋”
三、 結構體和指標
結構體內包含多個成員,這些成員之間在記憶體中是如何存放的呢?
比如:
struct fraction {
int num; // 整數部分
int denom; // 小數部分
};
struct fraction fp;
fp.num = 10;
fp.denom = 2;
這是一個定點小數結構體,它在記憶體佔 8 個位元組(這裡不考慮記憶體對齊),兩個成員域是這樣儲存的:
image-20201030214416842
我們把 10 放在了結構體中基地址偏移為 0 的域,2 放在了偏移為 4 的域。
接下來我們做一個正常人永遠不會做的操作:
((fraction*)(&fp.denom))->num = 5;
((fraction*)(&fp.denom))->denom = 12;
printf("%d\n", fp.denom); // 輸出多少?
上面這個究竟會輸出多少呢?自己先思考下噢~
接下來我分析下這個過程發生了什麼:
首先,&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
能把我搞崩潰,我估計很多同學現在就是這種情況?
其實,多級指標也沒那麼複雜,就是指標的指標的指標的指標......非常簡單。
今天就帶大家認識一下多級指標的本質。
首先,我要說一句話,沒有多級指標這種東西,指標就是指標,多級指標只是為了我們方便表達而取的邏輯概念。
首先看下生活中的快遞櫃:
這種大家都用過吧,豐巢或者超市儲物櫃都是這樣,每個格子都有一個編號,我們只需要拿到編號,然後就能找到對應的格子,取出裡面的東西。
這裡的格子就是記憶體單元,編號就是地址,格子裡放的東西就對應儲存在記憶體中的內容。
假設我把一本書,放在了 03 號格子,然後把 03 這個編號告訴你,你就可以根據 03 去取到裡面的書。
那如果我把書放在 05 號格子,然後在 03 號格子只放一個小紙條,上面寫著:「書放在 05 號」。
你會怎麼做?
當然是開啟 03 號格子,然後取出了紙條,根據上面內容去開啟 05 號格子得到書。
這裡的 03 號格子就叫指標,因為它裡面放的是指向其它格子的小紙條(地址)而不是具體的書。
明白了嗎?
那我如果把書放在 07 號格子,然後在 05 號格子 放一個紙條:「書放在 07號」,同時在03號格子放一個紙條「書放在 05號」
這裡的 03 號格子就叫二級指標,05 號格子就叫指標,而 07 號就是我們平常用的變數。
依次,可類推出 N 級指標。
所以你明白了嗎?同樣的一塊記憶體,如果存放的是別的變數的地址,那麼就叫指標,存放的是實際內容,就叫變數。
int a;
int *pa = &a;
int **ppa = &pa;
int ***pppa = &ppa;
上面這段程式碼,pa
就叫一級指標,也就是平時常說的指標,ppa
就是二級指標。
記憶體示意圖如下:
不管幾級指標有兩個最核心的東西:
- 指標本身也是一個變數,需要記憶體去儲存,指標也有自己的地址
- 指標記憶體儲存的是它所指向變數的地址
這就是我為什麼多級指標是邏輯上的概念,實際上一塊記憶體要麼放實際內容,要麼放其它變數地址,就這麼簡單。
怎麼去解讀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("%d\n", *array); // 輸出 10
printf("%d\n", array[0]); // 輸出 10
printf("%d\n", array[1]); // 輸出 9
printf("%d\n", *(array+1)); // 輸出 9
int *pa = array;
printf("%d\n", *pa); // 輸出 10
printf("%d\n", pa[0]); // 輸出 10
printf("%d\n", pa[1]); // 輸出 9
printf("%d\n", *(pa+1)); // 輸出 9
在記憶體中,陣列是一塊連續的記憶體空間:
第 0 個元素的地址稱為陣列的首地址,陣列名實際就是指向陣列首地址,當我們通過array[1]
或者*(array + 1)
去訪問陣列元素的時候。
實際上可以看做 address[offset]
,address
為起始地址,offset
為偏移量,但是注意這裡的偏移量offset
不是直接和 address
相加,而是要乘以陣列型別所佔位元組數,也就是: address + sizeof(int) * offset
。
學過彙編的同學,一定對這種方式不陌生,這是彙編中定址方式的一種:基址變址定址。
看完上面的程式碼,很多同學可能會認為指標和陣列完全一致,可以互換,這是完全錯誤的。
儘管陣列名字有時候可以當做指標來用,但陣列的名字不是指標。
最典型的地方就是在 sizeof
:
printf("%u", sizeof(array));
printf("%u", sizeof(pa));
第一個將會輸出 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: %p\n", pi);
void* pv = pi;
pi = (int*) pv;
printf("address of pi: %p\n", pi);
這兩次輸出的值都會是一樣:
平常可能很少會這樣去轉換,但是當你用 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
修飾指標- ...
基本上涵蓋了 C 語言最核心的知識。