// 部落格

C 指標入門與最常見的錯誤

  • C 語言
  • 指標
  • 函式參數

變數住在記憶體裡,每一格都有自己的門牌號碼(地址)。指標,就是「存門牌號碼」的變數。 這篇用最少的符號講清楚指標在做什麼,最後整理初學者最常踩的幾個坑。

先分清楚兩個符號

  • &x:取出變數 x 的地址。
  • int *p:宣告 p 是「存 int 地址」的變數。
  • *p:照著 p 存的地址走過去,讀或寫那一格。

宣告時的 * 表示「這是指標」;運算式裡的 *p 表示「去那個地址」。同一個符號,兩種意思。

& 與 * 實際跑一次

#include <stdio.h>

int main(void) {
    int x = 10;
    int *p = &x;  // p 存的是 x 的地址

    printf("x  = %d\n", x);
    printf("*p = %d\n", *p);

    *p = 20;      // 透過 p 改到 x
    printf("x  = %d\n", x);
    printf("p == &x ? %d\n", p == &x);
    return 0;
}

輸出

x  = 10
*p = 10
x  = 20
p == &x ? 1

*p = 20 沒有直接寫到 x,但因為 p 指向 x,改的就是 x 那一格。

為什麼 swap 一定要傳指標?

C 的參數是傳值:函式拿到的是一份副本,改副本不會影響呼叫端。想讓函式改到呼叫端的變數,就把地址傳進去,函式再用 *a、*b 改原本那一格。

#include <stdio.h>

void swap_value(int a, int b) {
    int t = a; a = b; b = t;  // 只換到副本
}

void swap_ptr(int *a, int *b) {
    int t = *a; *a = *b; *b = t;
}

int main(void) {
    int x = 3, y = 7;
    swap_value(x, y);
    printf("swap_value: x=%d y=%d\n", x, y);
    swap_ptr(&x, &y);
    printf("swap_ptr:   x=%d y=%d\n", x, y);
    return 0;
}

輸出

swap_value: x=3 y=7
swap_ptr:   x=7 y=3

同樣叫 swap,輸出完全不同。追蹤程式(例如 APCS 程式識讀的 C 題本)時,要能一眼看出參數是傳值還是傳地址。

最常見的錯誤一:沒初始化就用指標

錯誤寫法

int *p;   // p 指向哪裡?沒有人知道
*p = 10;  // 未定義行為:可能當掉、可能改壞別的資料

修正寫法

int x;
int *p = &x;    // 先指向一個真的變數
*p = 10;        // 等於 x = 10

int *q = NULL;  // 還沒有目標就先設 NULL
if (q != NULL) *q = 10;

編譯時加上 -Wall,gcc 會警告 'p' is used uninitialized。警告不是雜訊,要看。

最常見的錯誤二:陣列傳進函式後用 sizeof 算長度

陣列傳進函式時,函式拿到的其實是指向第一個元素的指標,所以在函式裡 sizeof(a) 算的是指標的大小,不是整個陣列。

錯誤寫法

int sum(int a[]) {
    int n = sizeof(a) / sizeof(a[0]);  // 錯:a 在這裡是指標
    ...
}

正確的做法是把長度一起傳進去。不打算修改陣列時,加上 const:

#include <stdio.h>

int sum(const int *a, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += a[i];  // a[i] 就是 *(a + i)
    }
    return total;
}

int main(void) {
    int arr[] = {1, 2, 3, 4};
    printf("sizeof(arr) = %zu\n", sizeof(arr));
    printf("sum = %d\n", sum(arr, 4));
    return 0;
}

輸出

sizeof(arr) = 16
sum = 10

sizeof(arr) = 16 是在 int 佔 4 bytes 的環境下的結果;在 main 裡 arr 還是陣列,所以算得到整個陣列的大小。

一次記起來

  • &x 拿地址,*p 照地址讀寫。
  • 參數是傳值;要改呼叫端的變數,就傳 &x。
  • 指標宣告時就給值:&變數 或 NULL。
  • 陣列傳進函式會變成指標,長度要另外傳。
  • 編譯加 -Wall,很多指標錯誤會被抓到。

想看更完整的說明,可以讀開源課本《C 程式設計》的〈指標〉一章;有問題歡迎到 Discord 發問。

資料來源

想有人逐行看你的程式?到 Discord 發問(免費),或看看我們的 線上程式家教與小班課。