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。
核心運算子:& 與 *
這兩個運算子是指標操作的基礎:
- 取址運算子 (Address-of Operator)
&:- 作用:取得變數的記憶體位址。
- 讀作:"...的位址"。
- 取值運算子 (Dereference Operator)
*:- 作用:存取該位址所指向的記憶體內容。
- 讀作:"...指向的值"。
- 注意:宣告時的
int *p的*只是型態標記,與運算子*p意義不同。
#include <stdio.h>
int main() {
int num = 42;
int *ptr = # // 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 放在 * 的前面或後面,意義完全不同,這是很多人的面試考題。
指向常數的指標 (Pointer to Constant): 保護內容不被修改。
const int *ptr = &a; // *ptr = 100; // 錯誤!不能修改內容 ptr = &b; // 可以!指標本身可以轉向常數指標 (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)
- 永遠初始化指標:
int *p = NULL;或int *p = &a;。 - 使用前檢查 NULL:確保指標有效再使用。
if (ptr != NULL) { // do something } - free 後設為 NULL:避免懸空指標。
free(ptr); ptr = NULL; - 善用 const:如果函式不應該修改傳入的指標內容,請加上
const。void print_array(const int *arr, int size);
指標就像手術刀,銳利且強大。只要遵守規則、小心使用,它就是你寫出高效能程式的最強武器。