M05-1 100 XP v1 Modern C ch. 11

指標是「帶型別的位址」:& 與 * 的記憶體檢視器之旅

先別執行——先下注

這一整章,只圍著三行程式打轉:

int  x = 0x1234;
char c = 'A';
int *p = &x;

第三行用 & 取出 x 的位址(address),交給 p 保管——這種「帶著去向的值」就是本章的主角:指標(pointer)。老規矩:先別執行,先下注。x 住在記憶體裡,有自己的位元組;c 也是。那 p 呢?

先預測再驗證

p 自己也住在記憶體裡嗎?它佔多少空間、內容是什麼?

押好了就往下走。九十秒內你會親眼對答案——而且看到的會比答案多:p 出生的那一刻,記憶體裡到底發生了什麼。

互動探索:打開記憶體

第一站:typed-address.c。下面的檢視器左邊是程式碼,右邊是這支程式的模擬記憶體——位址往下遞增,一列十六個位元組。

按「下一步」把前三行走完;每走一步,盯著右邊哪些位元組亮起黃框。走完後,點一下程式碼裡的 x,再點 c。

第0步:程式尚未開始執行,記憶體為未初始化狀態。

程式碼 typed-address.c
/* M05.1 example 1 — a pointer is a typed address.
* Phase 0 cvm subset only: int-family locals, &, *, assignment.
* The meminspect tour: is 4 bytes (LE: 34 12 00 00), is 1 byte (41),
* is its own 8-byte object holding 's address.
* main returns 0 iff both chapter predictions hold (native golden check). */
int main(void) {
int = 0x1234; /* LE bytes in the inspector: 34 12 00 00 */
char = 'A'; /* exactly one byte: 41 */
int * = &; /* 's own 8 bytes hold 's address */
* = 0x55667788; /* writes exactly 's 4 bytes; untouched */
return ( - 0x55667788) + ( - 'A');
}
變數(符號表)
名稱型別位址大小目前數值
int0x7ffdbffffffc4?(未初始化)
char0x7ffdbffffffb1?(未初始化)
int *0x7ffdbffffff08?(未初始化)

ⓘ 位址為教學模擬(x86-64 風格佈局);位元組內容依 x86-64 LP64 模型呈現

記憶體檢視器

位址遞增↓(每列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,像倒著放。這不是故障:

現在點程式碼裡的 p。它的反白足足有 8 個位元組——鉤子的答案揭曉:p 不是別名,它是一個自己佔格子的物件(object)。再看內容:那 8 個位元組拼出來的值,正是 x 那一列的位址。

接下來是本章最重要的一步。程式的下一行是 *p = 0x55667788;。先攔住你的手指:

先預測再驗證

執行 *p = 0x55667788; 之後,哪些位元組會亮起黃框?

回到上面的檢視器,按「下一步」執行這行寫入;再用時間軸拖回去重看一次黃框的落點。

寫入恰好覆蓋 x 的 4 個位元組,旁邊的註記指明了肇事者;c 的 41 一動也不動。p 沒有「裝下」新值——它只負責帶路,貨送進的是 x 家。

第二站:位址會旅行。set-through-pointer.c 把 &x 交給另一個函式的參數 t。逐步走進 set,先點 t 看它自己的 8 個位元組裝著什麼;執行 *t = 7; 時,盯著黃框出現在誰家。

第0步:程式尚未開始執行,記憶體為未初始化狀態。

程式碼 set-through-pointer.c
/* M05.1 example 2 — the address travels across a function call.
* Phase 0 cvm subset only: function definition/call/return, &, *.
* set() receives a copy of 's ADDRESS; * writes the caller's local.
* main returns 7 (native golden check). */
static void set(int *) {
* = 7; /* writes the CALLER's local */
}
int main(void) {
int = 1;
set(&);
return ; /* 7 */
}
變數(符號表)
名稱型別位址大小目前數值
int0x7ffdbffffffc4?(未初始化)
int *0x7ffdbfffffe88—(此步驟尚未生效)

ⓘ 位址為教學模擬(x86-64 風格佈局);位元組內容依 x86-64 LP64 模型呈現

記憶體檢視器

位址遞增↓(每列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 在本平台的大小。

第二件事:指標本身是物件。p 有自己的位址、自己的位元組,所以它能被改寫、能被比較,甚至能被另一個指標指著(&p——那是之後的故事)。「p 只是 x 的別名」這種想像,在檢視器面前站不住腳:你親眼看過它的格子。那麼,指標到底「長什麼樣子」?這裡標準與平台的分工很清楚:

現在可以把這一章濃縮成一句話了:變數不存在,存在的只有位址上的位元組;型別只是解讀眼鏡。「變數」「值」這些字眼是語言給你的方便說法;檢視器右邊那片格子才是程式的真相。x:int 覆蓋層是一副眼鏡,LE/BE 切換是換一副眼鏡,int * 與 char * 也是兩副度數不同的眼鏡——同一批位元組,誰來讀、讀出什麼,全看鏡片:

位址遞增 →
0x…ff1c   88 77 66 55
          ╰─ 戴 int 眼鏡(小端序):讀出 0x55667788,一次管 4 格 ─╯
          ╰─ 戴 char 眼鏡:只讀出 0x88,其餘三格不歸它管 ─╯

最後說一句誠實話。標準從未說過「區域變數住在堆疊上」——它只承諾變數在進入與離開區塊之間持續存在;「放在 0x7ffd 開頭的堆疊頁」是本平台的安排(前面探索時的⚙️框已經交代過),別把方言當成文法。

對照誤解目錄:MC-01「指標就是整數,可任意加減比較」。本章砍掉它的前半——指標是自帶格子的物件,而且型別決定 * 的觸碰範圍,這兩件事「整數」都解釋不了:兩個相等的整數沒有「指向」可言,更不會規定你一次讀寫幾個位元組。它的後半(p+1 到底跳多遠、一個位址能不能憑空捏造出來)要等指標算術與「來歷」登場才能補刀——先別急著對指標做加法。

標準筆記

本章的模型只壓在兩條條文上。這裡的任務是驗證,不是背誦:對照原文,確認剛才在檢視器裡看到的一切,正是標準承諾(或刻意不承諾)的。

挑戰

收網時間:四道預測題,規則不變——先押注,再到檢視器裡驗證。

載入第三個範例 two-pointers.c:同一個 a,被 p、q 兩個指標指著。先把程式步進到 *q = 0x5566; 之前停下。

第0步:程式尚未開始執行,記憶體為未初始化狀態。

程式碼 two-pointers.c
/* M05.1 example 3 (challenge) — two pointers, one object.
* Phase 0 cvm subset only: int locals, &, *, assignment.
* and hold the SAME address; write through is visible through .
* Note: 0x5566 fits in 2 bytes, but an int write touches all 4 of 's
* bytes (66 55 00 00) — challenge item 1 bait.
* main returns 0 iff the aliasing prediction holds (native golden check). */
int main(void) {
int = 0x0a0b0c0d;
int = 0;
int * = &;
int * = &; /* same address stored twice */
* = 0x5566; /* write through ... */
= *; /* ... read the same bytes via */
return - 0x5566;
}
變數(符號表)
名稱型別位址大小目前數值
int0x7ffdbffffffc4?(未初始化)
int0x7ffdbffffff84?(未初始化)
int *0x7ffdbffffff08?(未初始化)
int *0x7ffdbfffffe88?(未初始化)

ⓘ 位址為教學模擬(x86-64 風格佈局);位元組內容依 x86-64 LP64 模型呈現

記憶體檢視器

位址遞增↓(每列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; 會改變哪些位元組?

第二題把同樣的原理搬到新處境——兩個指標,一個物件:

先預測再驗證

② 接著執行 r = *p;。p 從頭到尾沒被改過,r 會拿到什麼?

第三題回頭找老朋友 MC-01 算帳,小心陷阱:

先預測再驗證

③ 檢視器裡,p 的內容和 a 的值一樣嗎?

最後一題走出這支程式,純用模型推:

先預測再驗證

④ 假想有個 char *k 也裝著 a 的位址(本章引擎還跑不了,用模型推):*k 會觸碰幾個位元組?

複習掛鉤

獵殺記錄:本章對 MC-01「指標就是整數」開了兩槍——指標是自帶位元組的物件;型別決定 * 的觸碰範圍。但它還沒死透:p+1 到底跳多遠、位址能不能憑空捏造,留給第 3 章〈指標算術〉與第 4 章補刀;M06 還要處理它的近親「陣列就是指標」。忘了的時候,回到上面的檢視器重演一次——重演比重讀有效。本章四個概念(帶型別的位址、取址、解參考、指標即物件)的複習題已入庫,之後會在複習佇列與你重逢。