指標(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);   /* 正確 */

練習題

  1. 宣告一個 double 變數與指向它的指標,透過指標把值改為原本的兩倍,印出前後結果。
  2. 寫一個函式 void add_ten(int *p),把指向的整數加 10;在 main 中驗證。
  3. 寫一個函式 void min_max(const int a[], int n, int *min, int *max), 同時回傳陣列的最小值與最大值(透過指標參數)。
  4. 寫一個函式 int safe_deref(const int *p, int fallback): 若 p 為 NULL 回傳 fallback,否則回傳 *p。
  5. 寫一個函式 void sort3(int *a, int *b, int *c), 利用 swap 讓三個變數由小到大排列(提示:最多三次比較交換)。
  6. (思考題)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:指標與陣列的深層關係