先別執行——先下注
這一整章,只圍著三行程式打轉:
int x = 0x 1234 ;
char c = 'A' ;
int * p = & x;
第三行用 & 取出 x 的位址(address),交給 p 保管——這種「帶著去向的值」就是本章的主角:指標(pointer)。老規矩:先別執行,先下注。x 住在記憶體裡,有自己的位元組;c 也是。那 p 呢?
🔮 先預測再驗證
p 自己也住在記憶體裡嗎?它佔多少空間、內容是什麼?
○ p 不佔記憶體,它只是 x 的另一個名字 ○ p 佔 4 個位元組,存的是 x 的值 0x1234 ○ p 佔 8 個位元組,存的是 x 的位址
押好了就往下走。九十秒內你會親眼對答案——而且看到的會比答案多:p 出生的那一刻,記憶體裡到底發生了什麼。
互動探索:打開記憶體
第一站:typed-address.c。下面的檢視器左邊是程式碼,右邊是這支程式的模擬記憶體——位址往下遞增,一列十六個位元組。
按「下一步」把前三行走完;每走一步,盯著右邊哪些位元組亮起黃框。走完後,點一下程式碼裡的 x,再點 c。
第0步:程式尚未開始執行,記憶體為未初始化狀態。
1 /* M05.1 example 1 — a pointer is a typed address.
2 * Phase 0 cvm subset only: int-family locals, &, *, assignment.
3 * The meminspect tour: x is 4 bytes (LE: 34 12 00 00), c is 1 byte (41),
4 * p is its own 8-byte object holding x 's address.
5 * main returns 0 iff both chapter predictions hold (native golden check). */
6 int main(void) {
7 int x = 0x1234; /* LE bytes in the inspector: 34 12 00 00 */
8 char c = 'A'; /* exactly one byte: 41 */
9 int *p = &x ; /* p 's own 8 bytes hold x 's address */
10 *p = 0x55667788; /* writes exactly x 's 4 bytes; c untouched */
11 return (x - 0x55667788) + (c - 'A');
12 }
名稱 型別 位址 大小 目前數值 x int0x7ffdbffffffc4 ?(未初始化) c char0x7ffdbffffffb1 ?(未初始化) p int *0x7ffdbffffff08 ?(未初始化)
ⓘ 位址為教學模擬(x86-64 風格佈局);位元組內容依 x86-64 LP64 模型呈現
記憶體檢視器 位元組序 LE(小端) BE(大端)
位址遞增↓(每列16個位元組)
位址
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+a
+b
+c
+d
+e
+f
0x7ffdbffffff0
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 將游標移至位元組可查看所屬變數;點選變數可反白其位元組。
?? 未初始化(灰+斜紋) 3c 已初始化(綠) 3c 本步寫入(黃色外框) 3c 選取的變數(反白) stack heap .data .rodata .text typed-address.c 的記憶體檢視器
看到了嗎:點 x 時反白的恰好是 4 個位元組,覆蓋層標著 x:int;點 c 只反白 1 個位元組。再湊近看 x 的內容——0x1234 在格子裡竟是 34 12 00 00,像倒著放。這不是故障:
⚙️ 平台事實 x86-64 SysV LP64(suzu-cvm 教學模擬)
本平台以小端序(little-endian)存放多位元組整數:數值最低的位元組放在最低的位址,所以
0x1234 看起來「倒著放」。把右上角的 LE 切到 BE 再看一次
x——同一批位元組被讀成了另一個數。 位元組從沒變,變的是讀法。
現在點程式碼裡的 p。它的反白足足有 8 個位元組——鉤子的答案揭曉:p 不是別名,它是一個自己佔格子的物件(object)。再看內容:那 8 個位元組拼出來的值,正是 x 那一列的位址。
⚙️ 平台事實 x86-64 SysV LP64(suzu-cvm 教學模擬)
「指標佔 8 個位元組」是本平台(LP64 資料模型)的選擇,不是 C
的規定。你也會發現這些區域變數的位址都以 0x7ffd
開頭——那是模擬的堆疊(stack)頁,由高位址往低位址成長。換一個平台,這兩件事都可能不同。
接下來是本章最重要的一步。程式的下一行是 *p = 0x55667788;。先攔住你的手指:
🔮 先預測再驗證
執行 *p = 0x55667788; 之後,哪些位元組會亮起黃框?
○ p 自己的 8 個位元組——新值存進了 p ○ x 的 4 個位元組——p 只帶路,寫入發生在 x 家 ○ x 的 4 個位元組加上 c 的 1 個位元組——值太大,溢進鄰居
回到上面的檢視器,按「下一步」執行這行寫入;再用時間軸拖回去重看一次黃框的落點。
寫入恰好覆蓋 x 的 4 個位元組,旁邊的註記指明了肇事者;c 的 41 一動也不動。p 沒有「裝下」新值——它只負責帶路,貨送進的是 x 家。
第二站:位址會旅行。set-through-pointer.c 把 &x 交給另一個函式的參數 t。逐步走進 set,先點 t 看它自己的 8 個位元組裝著什麼;執行 *t = 7; 時,盯著黃框出現在誰家。
第0步:程式尚未開始執行,記憶體為未初始化狀態。
1 /* M05.1 example 2 — the address travels across a function call.
2 * Phase 0 cvm subset only: function definition/call/return, &, *.
3 * set() receives a copy of x 's ADDRESS; *t writes the caller's local.
4 * main returns 7 (native golden check). */
5 static void set(int *t ) {
6 *t = 7; /* writes the CALLER's local x */
7 }
8
9 int main(void) {
10 int x = 1;
11 set(&x );
12 return x ; /* 7 */
13 }
名稱 型別 位址 大小 目前數值 x int0x7ffdbffffffc4 ?(未初始化) t int *0x7ffdbfffffe88 —(此步驟尚未生效)
ⓘ 位址為教學模擬(x86-64 風格佈局);位元組內容依 x86-64 LP64 模型呈現
記憶體檢視器 位元組序 LE(小端) BE(大端)
位址遞增↓(每列16個位元組)
位址
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+a
+b
+c
+d
+e
+f
0x7ffdbfffffe0
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 0x7ffdbffffff0
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 將游標移至位元組可查看所屬變數;點選變數可反白其位元組。
?? 未初始化(灰+斜紋) 3c 已初始化(綠) 3c 本步寫入(黃色外框) 3c 選取的變數(反白) stack heap .data .rodata .text set-through-pointer.c 的記憶體檢視器
t 住在 set 的格子裡,內容卻是 main 那邊 x 的位址;*t = 7 的黃框直接落在 x 家。位址被複製著旅行,型別(type)跟著位址走——到了異鄉,* 仍找得到回家的路,而且仍然恰好動 4 個位元組。
收個尾。整趟下來你應該已經發現:*p、*t 每一次都不多不少,恰好觸碰 4 個位元組。是誰決定了「4」?下一節,我們把這條規則攤開來講。
形式化:帶型別的位址
剛才你看到:*p 每一次讀寫都恰好落在 4 個位元組上,而 p 自己另佔 8 個位元組、內容是一個位址。這不是巧合,而是本章標題的全部內容——指標是「帶型別的位址」。
先給操作正式的名字:& 是取址運算子(address-of operator),對一個物件使用,產生「指向該物件」的指標值;* 是解參考(dereference)運算子,消費一個指標值,把你帶回位址上的那個物件。一個指標值因此帶著兩件資訊:位址 ——去哪裡;型別 ——到了之後,把多少個位元組當成一個什麼來讀寫。*p 永遠恰好動 4 個位元組,因為 p 的型別是 int *,而「4」是 int 在本平台的大小。
📜 標準 C17 6.5.3.2 Address and indirection operators
一元 & 產生其運算元的位址:對型別為 T 的物件取 &,得到「指向 T
的指標」型別的值。一元 *
做間接存取:運算元指向哪個物件,結果就是那個物件本身,型別回到
T。這是每一個符合標準的實作都必須遵守的契約。
查看 N2176 原文節錄 The unary & operator yields the address of its operand. If the operand has type “type”, the result has type “pointer to type”. [...] The unary * operator denotes indirection. If the operand points to a function, the result is a function designator; if it points to an object, the result is an lvalue designating the object. If the operand has type “pointer to type”, the result has type “type”. 一元 & 產生其運算元的位址:對型別 T 的運算元取 &,結果的型別是「指向 T 的指標」。一元 * 做間接存取:運算元指向哪個物件,結果就是指定該物件的左值,型別回到 T——解參考的觸碰範圍由指標的型別決定。
⚙️ 平台事實 x86-64 SysV LP64(suzu-cvm 教學模擬)
在檢視器裡,&x 顯示為一個十六進位的模擬位址;p 的型別是 int *,所以
*p 每次恰好讀寫 4 個位元組,並以小端序組合成值。位址的長相、4
這個數字,都是平台事實。
第二件事:指標本身是物件 。p 有自己的位址、自己的位元組,所以它能被改寫、能被比較,甚至能被另一個指標指著(&p——那是之後的故事)。「p 只是 x 的別名」這種想像,在檢視器面前站不住腳:你親眼看過它的格子。那麼,指標到底「長什麼樣子」?這裡標準與平台的分工很清楚:
📜 標準 C17 6.2.5p28 Types — pointer representations
標準只保證少數幾組指標型別共用同一種表示法(例如 void *
與字元指標);其餘型別的指標不必 有相同的表示法或對齊要求。換句話說,在標準層面,指標不是「一種整數」——它的長相是留給實作決定的。
查看 N2176 原文節錄 A pointer to void shall have the same representation and alignment requirements as a pointer to a character type. [...] Pointers to other types need not have the same representation or alignment requirements. 標準只保證少數幾組指標型別(如 void * 與字元指標)共用同一種表示法與對齊要求;其餘型別的指標「不必相同」。指標的長相是實作的事——「指標就是整數」在標準層面沒有依據。
⚙️ 平台事實 x86-64 SysV LP64(suzu-cvm 教學模擬)
本平台選擇 LP64:所有物件指標一律 8
個位元組、同一種表示法,於是指標「看起來就像個 64
位元整數」。記住:這是平台的方言,不是 C 的文法。
現在可以把這一章濃縮成一句話了:變數不存在,存在的只有位址上的位元組;型別只是解讀眼鏡。 「變數」「值」這些字眼是語言給你的方便說法;檢視器右邊那片格子才是程式的真相。x:int 覆蓋層是一副眼鏡,LE/BE 切換是換一副眼鏡,int * 與 char * 也是兩副度數不同的眼鏡——同一批位元組,誰來讀、讀出什麼,全看鏡片:
位址遞增 →
0x…ff1c 88 77 66 55
╰─ 戴 int 眼鏡(小端序):讀出 0x55667788,一次管 4 格 ─╯
╰─ 戴 char 眼鏡:只讀出 0x88,其餘三格不歸它管 ─╯
最後說一句誠實話。標準從未說過「區域變數住在堆疊上」——它只承諾變數在進入與離開區塊之間持續存在;「放在 0x7ffd 開頭的堆疊頁」是本平台的安排(前面探索時的⚙️框已經交代過),別把方言當成文法。
對照誤解目錄:MC-01「指標就是整數,可任意加減比較」。本章砍掉它的前半——指標是自帶格子的物件,而且型別決定 * 的觸碰範圍,這兩件事「整數」都解釋不了:兩個相等的整數沒有「指向」可言,更不會規定你一次讀寫幾個位元組。它的後半(p+1 到底跳多遠、一個位址能不能憑空捏造出來)要等指標算術與「來歷」登場才能補刀——先別急著對指標做加法。
標準筆記
本章的模型只壓在兩條條文上。這裡的任務是驗證,不是背誦:對照原文,確認剛才在檢視器裡看到的一切,正是標準承諾(或刻意不承諾)的。
📜 標準 C17 6.5.3.2 Address and indirection operators
原文(節錄):“The unary & operator yields the address of its operand. … The unary *
operator denotes indirection. If the operand points to an object, the result is an lvalue
designating the object. If the operand has type ‘pointer to type’, the result has type ‘type’.”
白話:& 產生運算元的位址;* 做間接存取——運算元指向哪個物件,結果就是那個物件本身,且結果的型別由指標的型別決定。你在檢視器裡看到「*p 恰好動 4 格」,那個「4」的源頭就在這句:型別跟著指標走,存取範圍跟著型別走。
查看 N2176 原文節錄 The unary & operator yields the address of its operand. If the operand has type “type”, the result has type “pointer to type”. [...] The unary * operator denotes indirection. If the operand points to a function, the result is a function designator; if it points to an object, the result is an lvalue designating the object. If the operand has type “pointer to type”, the result has type “type”. 一元 & 產生其運算元的位址:對型別 T 的運算元取 &,結果的型別是「指向 T 的指標」。一元 * 做間接存取:運算元指向哪個物件,結果就是指定該物件的左值,型別回到 T——解參考的觸碰範圍由指標的型別決定。
📜 標準 C17 6.2.5p28 Types — pointer representations
原文(節錄):“A pointer to void shall have the same representation and alignment
requirements as a pointer to a character type. … Pointers to other types need not have the
same representation or alignment requirements.”
白話:標準只點名少數幾組指標型別必須共用表示法,其餘的「不必相同」。也就是說,指標長什麼樣子、佔幾個位元組,是實作的事——「指標就是整數」這個說法,在標準裡找不到任何靠山。
查看 N2176 原文節錄 A pointer to void shall have the same representation and alignment requirements as a pointer to a character type. [...] Pointers to other types need not have the same representation or alignment requirements. 標準只保證少數幾組指標型別(如 void * 與字元指標)共用同一種表示法與對齊要求;其餘型別的指標「不必相同」。指標的長相是實作的事——「指標就是整數」在標準層面沒有依據。
挑戰
收網時間:四道預測題,規則不變——先押注,再到檢視器裡驗證。
載入第三個範例 two-pointers.c:同一個 a,被 p、q 兩個指標指著。先把程式步進到 *q = 0x5566; 之前停下。
第0步:程式尚未開始執行,記憶體為未初始化狀態。
1 /* M05.1 example 3 (challenge) — two pointers, one object.
2 * Phase 0 cvm subset only: int locals, &, *, assignment.
3 * p and q hold the SAME address; a write through q is visible through p .
4 * Note: 0x5566 fits in 2 bytes, but an int write touches all 4 of a 's
5 * bytes (66 55 00 00) — challenge item 1 bait.
6 * main returns 0 iff the aliasing prediction holds (native golden check). */
7 int main(void) {
8 int a = 0x0a0b0c0d;
9 int r = 0;
10 int *p = &a ;
11 int *q = &a ; /* same address stored twice */
12 *q = 0x5566; /* write through q ... */
13 r = *p ; /* ... read the same bytes via p */
14 return r - 0x5566;
15 }
名稱 型別 位址 大小 目前數值 a int0x7ffdbffffffc4 ?(未初始化) r int0x7ffdbffffff84 ?(未初始化) p int *0x7ffdbffffff08 ?(未初始化) q int *0x7ffdbfffffe88 ?(未初始化)
ⓘ 位址為教學模擬(x86-64 風格佈局);位元組內容依 x86-64 LP64 模型呈現
記憶體檢視器 位元組序 LE(小端) BE(大端)
位址遞增↓(每列16個位元組)
位址
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+a
+b
+c
+d
+e
+f
0x7ffdbfffffe0
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 0x7ffdbffffff0
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 將游標移至位元組可查看所屬變數;點選變數可反白其位元組。
?? 未初始化(灰+斜紋) 3c 已初始化(綠) 3c 本步寫入(黃色外框) 3c 選取的變數(反白) stack heap .data .rodata .text two-pointers.c 的記憶體檢視器
停好了嗎?第一題是探索的重演,只是換了數字:
🔮 先預測再驗證
① 執行 *q = 0x5566; 會改變哪些位元組?
○ a 的 4 個位元組全部重寫(66 55 00 00) ○ 只動 2 個位元組——0x5566 兩個位元組就裝得下 ○ q 自己的 8 個位元組
第二題把同樣的原理搬到新處境——兩個指標,一個物件:
🔮 先預測再驗證
② 接著執行 r = *p;。p 從頭到尾沒被改過,r 會拿到什麼?
○ 0x5566——p 與 q 裝著同一個位址,看的是同一批位元組 ○ 0x0a0b0c0d——透過 q 的寫入不會影響 p 讀到的值 ○ 無法預測
第三題回頭找老朋友 MC-01 算帳,小心陷阱:
🔮 先預測再驗證
③ 檢視器裡,p 的內容和 a 的值一樣嗎?
○ 一樣——指標存的就是它指向的變數的值 ○ 不一樣——p 存 a 的位址,a 存 0x5566 ○ 寫入前一樣,寫入後不一樣
最後一題走出這支程式,純用模型推:
🔮 先預測再驗證
④ 假想有個 char *k 也裝著 a 的位址(本章引擎還跑不了,用模型推):*k 會觸碰幾個位元組?
○ 4 個——位址一樣,行為就一樣 ○ 1 個——觸碰範圍由指標的型別決定 ○ 8 個——指標都佔 8 個位元組
複習掛鉤
獵殺記錄:本章對 MC-01「指標就是整數」開了兩槍——指標是自帶位元組的物件;型別決定 * 的觸碰範圍。但它還沒死透:p+1 到底跳多遠、位址能不能憑空捏造,留給第 3 章〈指標算術〉與第 4 章補刀;M06 還要處理它的近親「陣列就是指標」。忘了的時候,回到上面的檢視器重演一次——重演比重讀有效。本章四個概念(帶型別的位址、取址、解參考、指標即物件)的複習題已入庫,之後會在複習佇列與你重逢。