指標(pointer)是 C 語言最核心、也最容易讓初學者卻步的主題。 它的本質其實很單純:指標就是一個「存放記憶體位址」的變數。 掌握指標之後,你才能真正理解陣列、字串、動態記憶體與函式間的資料傳遞。
學習目標
- 理解「記憶體位址」的概念,知道每個變數都住在記憶體的某個位置
- 會使用取址運算子
&取得變數的位址 - 會使用間接取值(解參考)運算子
*透過指標讀寫變數 - 能正確宣告、初始化各種型別的指標
- 理解
NULL指標的意義,養成「用前檢查」的習慣 - 理解 C 的「傳值呼叫」限制,並能用指標讓函式修改呼叫端變數(經典
swap範例) - 明白為什麼
scanf("%d", &n)需要&,而字串不用
對照 K&R 章節
| 本單元主題 | K&R 2nd Edition |
|---|---|
| 指標與位址 | §5.1 Pointers and Addresses |
| 指標與函式引數 | §5.2 Pointers and Function Arguments |
| 傳值呼叫背景知識 | §1.8 Arguments — Call by Value |
觀念講解
1. 記憶體與位址
程式執行時,每個變數都占據記憶體中的一段空間,而每個位元組(byte)都有唯一的位址(address),
就像街道上的門牌號碼。以下是一個 int c = 7; 在記憶體中的示意圖(假設 int 佔 4 bytes):
位址: 0x7ff...5c8 0x7ff...5c9 0x7ff...5ca 0x7ff...5cb
+------------+------------+------------+------------+
變數 c: | 7(佔 4 個 byte) |
+------------+------------+------------+------------+
「c 的位址」指的是這段空間的起始位址(例如 0x7ff...5c8)。
2. 取址運算子 &
一元運算子 & 會取出運算元的位址:
int c = 7;
int *p; /* p 是「指向 int 的指標」 */
p = &c; /* p 現在存著 c 的位址,讀作「p 指向 c」 */
用 ASCII 圖表示這個關係:
p c
+-----------+ +---------+
| 0x...5c8 | -----> | 7 |
+-----------+ +---------+
存放 c 的位址 存放實際的值
注意:& 只能作用在「有位址的東西」(變數、陣列元素等),
不能寫 &(x+1) 或 &3,因為運算結果與常數沒有固定的記憶體位置。
3. 間接取值運算子 *
一元運算子 * 作用在指標上,取得「指標所指向的物件」:
int c = 7;
int *p = &c;
printf("%d\n", *p); /* 印出 7:透過 p 讀 c 的值 */
*p = 100; /* 透過 p 改 c 的值 */
printf("%d\n", c); /* 印出 100 */
*p 出現在等號左邊就是「寫入 p 指向的位置」,出現在右邊就是「讀出 p 指向的值」。
4. 指標的宣告
宣告 int *p; 的助記方式是:「*p 是一個 int」。這也解釋了為什麼多個指標要各自加星號:
int *p, *q; /* p、q 都是指標 */
int *a, b; /* 注意!a 是指標,b 只是普通 int(常見誤解) */
每種型別都有對應的指標型別,且型別必須匹配:
double d = 3.14;
double *pd = &d; /* 正確 */
int *pi = &d; /* 錯誤:型別不符,編譯器會警告 */
5. NULL 指標
NULL(定義在 <stddef.h>、<stdio.h> 等標頭)是一個保證「不指向任何物件」的特殊值。
未初始化的指標內容是垃圾值,直接解參考是未定義行為;好習慣是:
int *p = NULL; /* 暫時沒有目標就設為 NULL */
if (p != NULL) { /* 使用前先檢查 */
printf("%d\n", *p);
}
解參考 NULL 指標在多數平台會直接讓程式當掉(segmentation fault), 這反而比解參考垃圾指標「安靜地讀到錯誤資料」容易除錯。
6. 指標與函式引數:swap 經典範例
C 的函式呼叫是傳值(call by value):函式拿到的是引數的「複本」。 所以下面這個 swap 是無效的:
void swap_wrong(int x, int y) /* x、y 只是複本 */
{
int tmp = x;
x = y; /* 只交換了複本,呼叫端毫無變化 */
y = tmp;
}
正確做法是把「變數的位址」傳進去,讓函式透過指標操作呼叫端的變數:
void swap(int *px, int *py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
/* 呼叫端 */
swap(&a, &b); /* 傳位址進去 */
呼叫端: a = 1 b = 2
^ ^
| |
swap 內: px py <- 指標是位址的複本,
但透過它們仍能改到本尊
7. 為什麼 scanf 需要 &
scanf 要把讀到的值「寫回你的變數」,所以它需要變數的位址:
int n;
scanf("%d", &n); /* 給它 n 的位址,它才能把結果寫進 n */
如果忘了 &,scanf 會把 n 的(垃圾)值當成位址去寫入,造成未定義行為。
而讀字串到字元陣列時不用 &,因為陣列名本身就會轉換成首元素的位址(下一單元詳談):
char name[32];
scanf("%31s", name); /* name 已經是陣列的開始位址,不需要 & */
範例程式
| 檔案 | 內容 |
|---|---|
01_address_and_deref.c |
觀察變數位址、& 與 * 的基本用法、指標本身也有位址 |
02_swap.c |
傳值 vs 傳指標的對比,經典 swap 範例,並示範用指標回傳多個結果 |
03_null_and_scanf.c |
NULL 指標的檢查慣例、用 sscanf 示範為何需要 & |
編譯與執行(以第一個為例):
cc -std=c11 -Wall -Wextra -o /tmp/demo 01_address_and_deref.c
/tmp/demo
常見錯誤與陷阱
1. 解參考未初始化的指標
int *p; /* p 是垃圾值 */
*p = 42; /* 未定義行為!可能當掉、可能悄悄破壞資料 */
修正:宣告時就給目標或設 NULL:
int x;
int *p = &x; /* 或 int *p = NULL; 之後再指派 */
*p = 42;
2. 一行宣告多個指標時漏掉星號
int* a, b; /* b 不是指標!只是 int */
修正(並建議星號貼著變數名):
int *a, *b;
3. 回傳區域變數的位址
int *bad(void)
{
int local = 5;
return &local; /* local 在函式返回後就消失了(懸空指標) */
}
修正:改為回傳值、使用呼叫端提供的指標,或(之後會學的)動態配置。
4. scanf 忘記加 &
int n;
scanf("%d", n); /* 錯誤:把 n 的值當位址用 */
scanf("%d", &n); /* 正確 */
練習題
- 宣告一個
double變數與指向它的指標,透過指標把值改為原本的兩倍,印出前後結果。 - 寫一個函式
void add_ten(int *p),把指向的整數加 10;在main中驗證。 - 寫一個函式
void min_max(const int a[], int n, int *min, int *max), 同時回傳陣列的最小值與最大值(透過指標參數)。 - 寫一個函式
int safe_deref(const int *p, int fallback): 若p為 NULL 回傳fallback,否則回傳*p。 - 寫一個函式
void sort3(int *a, int *b, int *c), 利用 swap 讓三個變數由小到大排列(提示:最多三次比較交換)。 - (思考題)
int *p = &x; int **pp = &p;畫出x、p、pp三者的記憶體關係圖, 並回答**pp是什麼。
延伸閱讀
- K&R 2nd Edition,§5.1–5.2(本單元主軸)
- K&R §1.8:Call by Value 的原始說明
- comp.lang.c FAQ 第 4 章(Pointers):http://c-faq.com/ptrs/
- 下一單元
02-pointers-arrays:指標與陣列的深層關係