1 指標是什麼:位址(&)與解參考(*)的基本概念
指標變數存放的是「記憶體位址」,可以想成一張寫著門牌號碼的紙條——紙條本身不是房子,而是告訴你房子在哪裡。&x 取得變數x的位址(把門牌號碼抄到紙條上);*p 則是「解參考(dereference)」,拿著紙條去敲門,取得指標p所指向位址中存放的值。
int x = 10;
int *p = &x; // p存放x的位址(紙條上寫著x的門牌號碼)
printf("%d\n", x); // 10(變數本身的值)
printf("%d\n", *p); // 10(透過p解參考,拿到x的值)
*p = 20; // 透過p「敲門」,直接修改x的內容
printf("%d\n", x); // 20(x真的被改了!)
10
20
| 符號 | 名稱 | 作用 |
|---|---|---|
&x | 取址運算子 | 取得變數 x 的記憶體位址 |
int *p | 指標宣告 | 宣告 p 是一個「存放 int 位址」的指標變數 |
*p | 解參考運算子 | 取得(或修改)p 所指向位址中存放的值 |
2 傳值呼叫 vs 傳址呼叫:函式如何真正修改呼叫端變數
函式參數若用一般型態(如 int)傳遞,函式拿到的只是原始變數的一份複本,函式內對參數的修改不會影響呼叫端的原始變數(傳值呼叫,call by value);若要讓函式能真正修改呼叫端變數,必須改用指標型態當參數(傳址呼叫,call by reference)。
void inc_by_value(int x) { x = x + 1; } // 只改到函式內的複本,main的變數不受影響
void inc_by_reference(int *x) { *x = *x + 1; } // 透過位址找到原始變數並修改它
int main(void) {
int a = 10;
inc_by_value(a); printf("%d\n", a); // 10(沒變)
inc_by_reference(&a); printf("%d\n", a); // 11(真的改到了)
}
11
| 呼叫方式 | 函式參數宣告 | 呼叫時怎麼傳 | 能否修改原始變數 |
|---|---|---|---|
| 傳值呼叫 | void f(int x) | f(a) | ❌ 不能,只改到複本 |
| 傳址呼叫 | void f(int *x) | f(&a) | ✅ 能,透過位址找到原始變數 |
要讓一個原本用一般型態傳遞的函式,改成能真正修改呼叫端變數,必須同時做兩件事:呼叫時把引數改成 &變數(傳位址),並且函式參數宣告改成指標型態、函式內所有存取都要加上 * 解參考。只改其中一邊、沒改另一邊,都無法達成傳址呼叫的效果。
3 指標的型態相容性:指標必須跟它指向的資料型態一致
指標宣告的型態(例如 int *p)代表它只能合法指向該型態的位址。把不同型態的位址(或直接把一個「值」而非「位址」)指定給指標,會產生型態不相容的編譯警告或錯誤。
int *p;
char MyName[] = {'A'};
int MyAge;
double MyWeight;
p = &MyAge; // ✅ &MyAge 型態是 int*,跟 p 完全相符
// p = MyName; // ❌ MyName 退化成 char*,跟 int* 不符
// p = &MyName[0]; // ❌ 同樣是 char*,跟 int* 不符
// p = MyWeight; // ❌ MyWeight 是 double「值」,不是位址,型態錯誤更明顯
1. 右邊是不是一個「位址」?(有沒有 &,或本身就是陣列名稱/指標變數)——如果右邊是一般變數的「值」,型態一定不符。
2. 這個位址指向的資料型態,跟指標宣告的型態是否完全一致?(int* 只能指向 int,char* 只能指向 char,以此類推)
4 指標算術(pointer arithmetic)與陣列名稱的關係
陣列名稱在大部分情況下會自動退化成「指向第一個元素的指標」。p+1 不是位址加1個byte,而是加「1個該型態大小」——編譯器會自動依指標宣告的型態換算要跳過幾個bytes。
int arr[3] = {100, 200, 300};
int *p = arr; // 陣列名稱會自動退化成指向第一個元素的指標
printf("%d\n", *p); // 100
printf("%d\n", *(p + 1)); // 200,指標+1會依型態大小跳過一個int(通常4 bytes)
printf("%d\n", p[2]); // 300,p[i]其實就是*(p+i)的另一種寫法
200
300
如果指標一開始就指向陣列中間某個位置(例如 int *p2 = &Data[2]),那麼 *(p2+3) 指向的其實是 Data[2+3]=Data[5]——位移量是從指標當下指向的位置往後算,不是從陣列的索引0開始算。
5 雙重指標(指標的指標)
指標本身也是一個變數,也會佔用一塊記憶體、也有自己的位址——所以「指向指標的指標」(int **pp)也是合法的。**pp 要先解一次參考拿到 p,再解一次參考才拿到最原始的 x。
int x = 5;
int *p = &x;
int **pp = &p; // pp存放的是「p這個指標變數」的位址
**pp = 99; // 透過雙重解參考,一路改到最原始的x
printf("%d\n", x); // 99
pp 是一張寫著「p這張紙條放在哪裡」的紙條;*pp 先照第一張紙條找到 p,**pp 再照 p 上寫的門牌號碼繼續找下去,最終找到 x。雙重指標常用在函式需要「修改一個指標本身所指向的位置」的情境(例如讓函式能改變呼叫端某個指標要指向哪裡)。
6 指標互相指向時的連鎖修改追蹤(統測最愛的複合陷阱)
當兩個指標分別指向兩個不同的變數,並依序透過解參考賦值時,後面那一行讀到的一定是「前一行執行完之後」的最新值,不是變數最原始的初始值。這是統測指標題最容易失分的地方。
int i1 = 1, i2 = 2;
int *p1 = &i2, *p2 = &i1; // p1指向i2,p2指向i1
*p1 = i1 + 1; // 把 i1+1 寫進 i2(此時i1還是原始值1)
*p2 = i2 + 2; // 把 i2+2 寫進 i1(此時i2已經是上一行更新後的值!)
1. *p1 = i1 + 1;:p1指向i2,所以這行是在修改i2。等號右邊的 i1 此時還是初始值1,所以i2被設為 1+1=2(跟原本的值剛好相同,容易誤以為「沒有變化」)。
2. *p2 = i2 + 2;:p2指向i1,所以這行是在修改i1。等號右邊的 i2,用的是上一行執行完之後的最新值2,不是最初的2(雖然數字剛好一樣,但推導邏輯不同),所以i1被設為 2+2=4。
最終 i1=4,i2=2。
「先看指標指向誰,再看等號用舊值還新值」——每一行賦值前,先確認左邊 *p 實際上是在修改哪個變數;等號右邊出現的變數,一律用「執行到這一行為止」最新的值,不是程式一開始的初始值。
7 111~115 歷屆統測真題全選項深度剖析(共 3 題)
以下收錄統測專業科目(二)近年全部 3 題指標真題。每題均提供高解析度原始考題掃描圖、互動選項按鈕與所有選項逐項深入解析:
int sum=1, x=10;
int inc(int xin){
int sum=2;
sum = sum + xin;
xin++;
return (sum);
}
int main(){
int sum = 3;
sum=inc(x);
printf("%d, %d", sum, x);
return 0;
}
🔍 四個選項逐項深度剖析
&x,inc() 的參數宣告改成指標型態 int *xin 接收這個位址,並在函式內用解參考 *xin 存取/修改實際指向的值(所有原本用 xin 的地方都改用 *xin),這樣函式內的修改才會真正反映到 main() 的 x 身上,選 (A)。*x 是錯誤的:x 本身已經是一個 int 變數的「值」,不是指標,對它取 *(解參考)沒有意義且無法通過編譯;而 inc() 裡把 xin 改成 &xin 也是方向錯誤——這個修改方式把「呼叫端該做的事」跟「函式內部該做的事」對調了,&(取址)與 *(解參考)分別該用在哪一邊完全弄反。&x 是正確的(要傳位址),但 inc() 內把 xin 改成 &xin 卻是錯的:函式參數要宣告成指標型態(int *xin)才能接收位址,並且要用 *xin(解參考)才能存取、修改指標所指向的值;&xin 是在對參數本身取址,語意跟「透過位址修改原始變數」完全不同,無法達成傳址呼叫。*x 與 inc() 內的 *xin 同樣方向錯誤:呼叫端要傳的是位址(&x),不是對一個普通 int 變數解參考(*x 不合語法);即使函式內部正確使用了 *xin,呼叫端沒有真的傳入位址,函式收到的仍然只是一個值的複本,無法修改到 main() 中的原始 x。int *p;
char MyName[] = {'A'};
int MyAge;
double MyWeight;
【1】
🔍 四個選項逐項深度剖析
char* 型態的指標,跟 p 宣告的 int* 型態不符——把 char* 指定給 int*,型態不相容,編譯器會產生警告(甚至在較嚴謹的設定下報錯),並不是「不會產生任何錯誤或警告」。&MyName[0] 是對 char 陣列的第一個元素取址,型態同樣是 char*,跟 p 的 int* 不符,理由跟選項A相同——只是換了一種寫法表達同一個 char 位址,型態不相容的問題並沒有解決。double 型態的「值」,不是位址,把一個 double 值直接指定給指標變數 p,型態錯誤的程度比選項A、B更明顯(連「位址」都不是),一定會產生編譯錯誤。&MyAge 是對 int 型態變數取址,得到的型態是 int*,跟 p 宣告的 int* 完全相符,型態相容,可以合法賦值,編譯時不會產生任何錯誤或警告訊息,選 (D)。int i1 = 1, i2 = 2;
int *p1 = &i2, *p2 = &i1;
*p1 = i1 + 1; *p2 = i2 + 2;
🔍 四個選項逐項深度剖析
*p1 = i1+1,即把 i2 設為 1+1=2(i1此時還沒被修改,仍是1;巧合的是 i2 原本也是2,看起來像沒變化)。接著執行 *p2 = i2+2,此時 i2 已經被上一行更新成2,故把 i1 設為 2+2=4。最終 i1=4,選 (A)。*p1 指的是 p1 所指向的內容,也就是 i2,而 i2 最終的值是2(不是4,詳見正確答案的推導),這個選項把答案(i1=4)誤套到了錯誤的變數身上——*p1 其實是 i2,不是 i1。i1+1=2,之後就沒有任何敘述再修改過 i2),這個選項把 i1 的最終值(4)誤植到了 i2 身上,兩個變數的答案對調了。*p2,等同於 i1=4)跟「p2這個指標變數本身的值變成4」搞混了——指標變數存放的是位址,不會因為 *p2=4 這樣的賦值而讓 p2 本身的內容變成數字4。8 高職段考與模擬精選實戰題(5 題全選項解析)
透過以下 5 題精選模擬試題,全面檢驗你對取址/解參考、傳值傳址、型態相容性與雙重指標的掌握度:
int a = 5, b = 8;
int *pa = &a, *pb = &b;
*pa = *pb + 1;
printf("%d %d", a, b);
🔍 四個選項逐項深度剖析
*pa = *pb + 1; 把 pb 所指向的值(也就是 b=8)加1,指定給 pa 所指向的變數(也就是 a)。所以 a 變成 8+1=9,b 全程沒有被任何敘述修改,維持8。輸出 "9 8",選 (A)。*pa = *pb + 1; 這行賦值沒有真的生效,以為 a 維持原值5。但這行敘述明確透過 pa 指標把 a 的內容改成了 *pb+1=9。*pb+1 誤算成了 *pa+1(把 pa 原本指向的 a 值5加1得到6),搞混了到底該用哪一個指標所指向的值來計算——程式碼明確寫的是 *pb(也就是 b 的值8),不是 *pa。void modify(int n) {
n = n * 10;
}
int main(void) {
int x = 3;
modify(x);
printf("%d", x);
return 0;
}
🔍 四個選項逐項深度剖析
&x 呼叫)時才會發生。本題的 modify() 使用的是一般 int 參數,屬於傳值呼叫,x 不會被改變。int *p;、float f = 3.14;、int n = 5;,下列哪一行程式碼放入空格處,編譯時不會產生型態不相容的警告?int *p;
float f = 3.14;
int n = 5;
/* 這裡要放一行程式碼 */
🔍 四個選項逐項深度剖析
&f 是對 float 型態變數取址,得到的型態是 float*,跟 p 宣告的 int* 不相容,型態不匹配。&n 是對 int 型態變數取址,得到的型態是 int*,跟 p 宣告的 int* 完全相符,型態相容,不會有任何警告,選 (C)。&,把一個整數值直接指定給指標變數仍然是型態錯誤——指標要存放的是位址,不是數值本身。double d[3] = {1.5, 2.5, 3.5};
double *p = d;
printf("%.1f %.1f", *(p+2), *p);
🔍 四個選項逐項深度剖析
*(p+2) 等同於 d[2]=3.5;*p 等同於 d[0]=1.5(p 目前指向的第一個元素,沒有額外位移)。依 printf 引數順序輸出 "3.5 1.5",選 (A)。*p 當成 d[1]=2.5,而不是 p 當下直接指向的 d[0]=1.5。*p 誤算成了 d[1]=2.5,而不是 p 目前指向位置的值 d[0]=1.5——*p 就是解一次參考,沒有額外的位移動作。*(p+2) 誤算成了某種「重複套用型態大小換算」後放大的數值(3.5的兩倍剛好是7.0)。但指標算術的 +2 已經自動依型態大小換算好要跳過的位元組數,直接對應到 d[2] 的值3.5,不需要再手動乘上型態大小做二次換算。int x = 1, y = 2;
int *p = &x;
int **pp = &p;
*p = y;
**pp = **pp + 5;
printf("%d %d", x, y);
🔍 四個選項逐項深度剖析
*p = y; 把 y 的值2指定給 p 所指向的 x,x 變成2。**pp 是雙重解參考:pp 存放 p 的位址,*pp 拿到 p(也就是 &x),再解一次參考 **pp 就是 x。所以 **pp = **pp + 5; 等同於 x = x + 5 = 2+5 = 7。y 全程沒有被任何敘述修改,維持2。最終輸出 "7 2",選 (A)。*p = y; 這行賦值沒有真的生效,以為 x 維持原始值1,忽略了這行明確透過 p 把 x 的內容改成了 y 的值2。*p=y 讓 x 變成2,但誤以為 **pp = **pp + 5; 這行沒有真的執行加5的動作,忽略了 **pp 就是 x 的另一種寫法,這行確實會讓 x 再加5,變成7。**pp 這種雙重解參考的寫法會同時影響到 x 和 y 兩個變數,但 pp 從頭到尾只指向 p、而 p 從頭到尾只指向 x,兩層解參考最終只會動到 x 一個變數,y 不會受到任何影響,應維持原值2。9 考前衝刺 10 秒速記口訣 & 核心知識檢核清單
& 從變數取得位址(做紙條);* 從位址取得(或修改)內容(照紙條找東西)。
呼叫時傳 &變數,函式參數宣告成指標型態並用*存取——兩邊都要改,只改一邊無效。
int* 只能指向 int 的位址;把值(非位址)或不同型態的位址指定給指標都會型態不符。
p+1 不是加1個byte,是加「1個該型態的大小」,不需要手動乘型態大小做二次換算。
**pp 要先解一次拿到p,再解一次拿到x,兩層各自對應一次跳轉,不會同時影響其他變數。
每行先確認*p改的是哪個變數,等號右邊一律用「執行到當下」的最新值,不是最初的初始值。