C 語言指標 (Pointers)

指標 (Pointer) 是 C 語言的靈魂,賦予了程式設計師直接操作記憶體的能力。它是高效能程式設計的關鍵,但也是初學者的夢魘與 Bug 的溫床。本章節將帶你深入理解指標的運作原理、最佳實踐與常見陷阱。

什麼是指標?

簡單來說,指標就是一個「儲存記憶體位址」的變數

  • 一般變數 (如 int a = 10;):儲存的是資料數值
  • 指標變數 (如 int *p = &a;):儲存的是資料的記憶體位址

記憶體圖解

想像記憶體是一排連續的格子,每個格子都有一個編號 (位址)。

變數名稱:    a           p
           +----+      +---------+
記憶體位址: | 10 |      | 0x1000  |
           +----+      +---------+
位址編號:  0x1000      0x2000
  • 變數 a 住在位址 0x1000,裡面放著數值 10
  • 變數 p 是一個指標,它住在 0x2000,裡面放著 a 的位址 0x1000
  • 我們說:p 指向 a

核心運算子:&*

這兩個運算子是指標操作的基礎:

  1. 取址運算子 (Address-of Operator) &
    • 作用:取得變數的記憶體位址。
    • 讀作:"...的位址"。
  2. 取值運算子 (Dereference Operator) *
    • 作用:存取該位址所指向的記憶體內容。
    • 讀作:"...指向的值"。
    • 注意:宣告時的 int *p* 只是型態標記,與運算子 *p 意義不同。
#include <stdio.h>

int main() {
    int num = 42;
    int *ptr = &num; // ptr 存了 num 的位址

    printf("num 的值: %d\n", num);        // 42
    printf("num 的位址: %p\n", &num);     // 例如 0x7ffd...
    printf("ptr 存的值: %p\n", ptr);      // 等於 &num
    
    // 透過指標修改 num
    *ptr = 100; // 前往 ptr 指向的位址,把那裡的東西改成 100
    printf("修改後的 num: %d\n", num);     // 100

    return 0;
}

指標的型態與運算 (Pointer Arithmetic)

為什麼指標需要型態 (如 int*, double*)?既然存的都是位址 (在 64-bit 系統上都是 8 bytes),為什麼不能統一用一種型態?

因為型態決定了「指標運算的步伐」與「讀取的範圍」。

int a = 10;
int *ip = &a;

double b = 3.14;
double *dp = &b;

// 假設 ip = 1000, dp = 2000
ip++; // ip 變成 1004 (增加了 sizeof(int) = 4)
dp++; // dp 變成 2008 (增加了 sizeof(double) = 8)
  • ptr + 1 實際上是 ptr + sizeof(type)
  • *ptr 取值時,int* 會讀取 4 bytes,char* 只會讀取 1 byte。

指標與陣列的關係

在 C 語言中,陣列與指標密不可分。陣列名稱 (Array Name) 在大多數情況下,會退化 (Decay) 成指向第一個元素的指標常量

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 等同於 int *p = &arr[0];

// 以下寫法效果相同
printf("%d\n", arr[2]);     // 30 (陣列索引法)
printf("%d\n", *(p + 2));   // 30 (指標運算法)
printf("%d\n", *(arr + 2)); // 30 (陣列名也是指標)
printf("%d\n", p[2]);       // 30 (指標也可以用索引法!)

指標與函式 (Call by Reference)

C 語言預設是 Call by Value (傳值),函式內的變數是複製品。要讓函式能修改外面的變數,必須使用指標模擬 Call by Reference (傳址)。

範例:交換兩個變數 (Swap)

// [錯誤] 傳值:只交換了複製品,不會影響原本變數
void swap_wrong(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

// [正確] 傳址:接收位址,直接修改該位址的內容
void swap(int *a, int *b) {
    int temp = *a; // 取出 a 指向的值
    *a = *b;       // 把 b 指向的值 寫入 a 指向的位址
    *b = temp;     // 把 temp 寫入 b 指向的位址
}

int main() {
    int x = 1, y = 2;
    swap(&x, &y); // 傳入位址
    printf("x=%d, y=%d\n", x, y); // x=2, y=1
    return 0;
}

const 與指標

const 放在 * 的前面或後面,意義完全不同,這是很多人的面試考題。

  1. 指向常數的指標 (Pointer to Constant): 保護內容不被修改。

    const int *ptr = &a; 
    // *ptr = 100; // 錯誤!不能修改內容
    ptr = &b;      // 可以!指標本身可以轉向
    
  2. 常數指標 (Constant Pointer): 保護指標不被修改 (不能轉向)。

    int * const ptr = &a;
    *ptr = 100;    // 可以!內容可以改
    // ptr = &b;      // 錯誤!不能轉向
    

通用指標 (void* Pointer)

void* 是一種特殊的指標,代表「不知道指向什麼型態」。

  • 可以指向任何型態的位址。
  • 不能進行 * 取值運算 (編譯器不知道要讀幾個 byte)。
  • 不能進行 ++-- 運算。
  • 必須先強制轉型 (Cast) 成具體型態才能使用。
int n = 10;
void *p = &n;
// printf("%d", *p); // 錯誤
printf("%d", *(int*)p); // 正確:轉型成 int* 後再取值

常見陷阱 (Common Pitfalls)

使用指標時務必小心以下情況,否則程式會直接當掉 (Segmentation Fault)。

(1) 未初始化的指標 (Wild Pointer)

宣告指標後如果沒有賦值,它可能指向記憶體的任何地方。

int *p; 
*p = 10; // 危險!p 可能指向系統核心或其他程式,導致崩潰

修正:宣告時務必初始化,若暫時不用則設為 NULL

(2) 懸空指標 (Dangling Pointer)

指向的記憶體已經被釋放了,但指標還留著。

int *func() {
    int a = 10;
    return &a; // 危險!a 是區域變數,函式結束後也會跟著消失
}
// 呼叫 func() 後得到的指標指向無效區域

(3) 記憶體洩漏 (Memory Leak)

使用 malloc 配置記憶體後,忘記 free。這會導致伺服器隨著時間推移記憶體耗盡。

最佳實踐 (Best Practices)

  1. 永遠初始化指標int *p = NULL;int *p = &a;
  2. 使用前檢查 NULL:確保指標有效再使用。
    if (ptr != NULL) {
        // do something
    }
    
  3. free 後設為 NULL:避免懸空指標。
    free(ptr);
    ptr = NULL;
    
  4. 善用 const:如果函式不應該修改傳入的指標內容,請加上 const
    void print_array(const int *arr, int size);
    

指標就像手術刀,銳利且強大。只要遵守規則、小心使用,它就是你寫出高效能程式的最強武器。