指針將 C 語言從「高級組合語言」昇華為**系統程式設計語言**。本文從**記憶體存儲模型**出發,系統構建指針知識體系:基礎語法、陣列關係、字串操作、函數指針、多級指針、動態記憶體、main 參數、常見陷阱與最佳實踐。
int a = 42;
// 編譯器分配 4 位元組空間 (假設 32 位 int)
// 假設地址為 0x7ffc1234
// 記憶體視圖:
// 位址: 0x7ffc1234 0x7ffc1235 0x7ffc1236 0x7ffc1237
// 數值: 0x2A 0x00 0x00 0x00 (小端序)
char *pc; // 1 byte, 步長 1
int *pi; // 4 bytes, 步長 4
double *pd; // 8 bytes, 步長 8
void *pv; // 無類型指針,不可解引用,不可算術,通用持有地址
int a[5] = {10, 20, 30, 40, 50};
int *p = a; // p = &a[0]
// 1. 下標法
a[i] // *(a + i)
// 2. 指針算術 + 解引用
*(a + i) // a 退化為指針
*(p + i) // 顯式指針
// 3. 指針下標 (指針也可用 []!)
p[i] // *(p + i)
// 4. 陣列名下標 (本質相同)
a[i] // *(a + i)
// 風格 1: 索引
for (int i = 0; i < n; i++) sum += a[i];
// 風格 2: 指針移動 (經典 C 風格)
for (int *p = a; p < a + n; p++) sum += *p;
// 風格 3: 末尾哨兵 (C99 靈活數組/指針範圍)
int *end = a + n;
for (int *p = a; p != end; p++) sum += *p;
int matrix[3][4]; // 3 行 4 列
// 陣列名退化: 指向「含 4 個 int 的陣列」的指針
int (*p)[4] = matrix; // 類型: int (*)[4]
// p+1 跳過一整行 (4*4=16 bytes)
// 訪問 matrix[i][j]:
matrix[i][j] // *(*(matrix + i) + j)
*(*(p + i) + j) // 顯式指針版
p[i][j] // 指針也可雙下標!
char str1[] = "hello"; // 陣列: 栈上, 可修改, 大小 6 (含 \0)
char *str2 = "hello"; // 指針: 指向唯讀區 (文字段/常量區), **不可修改**, 大小 8/4 (指針大小)
// str1[0] = 'H'; // ✓ 合法
// str2[0] = 'H'; // ✗ 未定義行為 (段錯誤/SIGSEGV)
// strlen
size_t my_strlen(const char *s) {
const char *p = s;
while (*p) p++;
return p - s; // 指針相減得元素個數
}
// strcpy
char *my_strcpy(char *dest, const char *src) {
char *d = dest;
while ((*d++ = *src++)); // 賦值 + 遞增 + 判斷 \0 (0 為假)
return dest;
}
// strcmp
int my_strcmp(const char *s1, const char *s2) {
while (*s1 && *s1 == *s2) { s1++; s2++; }
return *(unsigned char*)s1 - *(unsigned char*)s2; // 避免 char 有號溢位
}
void print_upper(const char *s) { // const 承諾不修改
while (*s) putchar(toupper(*s++));
}
// 傳遞的是首字元地址,函數內部可任意移動指針,不影響實參
int (*pf)(int, int); // pf 是指針,指向「接受兩個 int,返回 int」的函數
// 讀法: pf → *pf → (*pf)(int, int) → int
// typedef 簡化
typedef int (*BinaryOp)(int, int);
BinaryOp op = add;
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int (*pf)(int, int) = add; // 函數名自動退化為地址
// int (*pf)(int, int) = &add; // 顯式取址也可
int result = pf(3, 5); // 透過指針調用
// int result = (*pf)(3, 5); // 顯式解引用調用 (等價)
// qsort 回調經典原型
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
// 策略表
typedef void (*Handler)(int);
Handler handlers[] = {handle_read, handle_write, handle_close};
handlers[event_type](fd);
int a = 10;
int *p = &a;
int **pp = &p;
// 解引用層層遞進:
**pp // = *p = a = 10
*pp // = p = &a
pp // = &p
// 指針陣列: 每行獨立分配,長度可不同 (鋸齒陣列)
char *names[] = {"Alice", "Bob", "Charlie"}; // char *[]
// 記憶體: 連續指針陣列 → 離散字串
// 二維陣列: 連續塊,每行固定長度
char names2[3][10] = {"Alice", "Bob", "Charlie"}; // char [3][10]
// 記憶體: 30 bytes 連續
| 函數 | 原型 | 行為 | 錯誤返回 |
|---|---|---|---|
| malloc | `void *malloc(size_t size)` | 分配**未初始化**記憶體 | NULL |
| calloc | `void *calloc(size_t nmemb, size_t size)` | 分配並**清零** (nmemb * size) | NULL |
| realloc | `void *realloc(void *ptr, size_t size)` | 調整大小 (可能移動、複製、釋放舊) | NULL (舊記憶體保留) |
| free | `void free(void *ptr)` | 釋放,**ptr 必須來自 malloc 系** | 無 |
// C 語言: void* 可隱式轉換為任何指針類型,**無需強制轉型**
int *p = malloc(10 * sizeof(int)); // 推薦寫法
// int *p = (int*)malloc(...); // C++ 需要,C 不需要且掩蓋錯誤 (忘記 include stdlib.h)
// 分配結構體
struct Node *node = malloc(sizeof(struct Node));
// 或更安全 (不依賴類型名):
struct Node *node = malloc(sizeof *node);
| 錯誤 | 代碼 | 後果 | 防禦 |
|---|---|---|---|
| 未檢查 NULL | `p = malloc(...); p[0]=1;` | 段錯誤 | `if (!p) handle_error();` |
| 釋放後使用 | `free(p); *p=1;` | UAF (Use After Free) | `free(p); p = NULL;` |
| 重複釋放 | `free(p); free(p);` | 堆破壞 | `free(p); p = NULL;` |
| 釋放非堆指針 | `int a; free(&a);` | 未定義行為 | 僅 free malloc 返回值 |
| 記憶體洩漏 | 分配無釋放 | 耗盡記憶體 | 所有權清晰、RAII、Valgrind/ASan 檢測 |
| 柔性陣列成員誤用 | `struct {int n; int data[];} *s = malloc(sizeof(*s));` | 未分配 data 空間 | `malloc(sizeof(*s) + n * sizeof(int))` |
int main(int argc, char *argv[]) // 等價於 char **argv
// argc: 參數個數 (含程式名)
// argv: 指針陣列,argv[0] = 程式名,argv[argc] = NULL (保證)
// 實際記憶體: 連續指針 → 連續字串 (環境變數緊隨其後)
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-h") == 0) usage();
else if (strcmp(argv[i], "-o") == 0 && i+1 < argc) output = argv[++i];
else input = argv[i];
}
// 進階: getopt/getopt_long (POSIX 標準選項解析)
const int *p1; // 指向 const int 的指針: *p1 不可改,p1 可改 (底層 const)
int *const p2; // const 指針指向 int: p2 不可改,*p2 可改 (頂層 const)
const int *const p3; // 雙重 const: 都不可改
int const *p4; // 同 p1 (const 在 * 左側修飾 *指向的類型)
口訣:`const` 修飾**右側**最近的實體 (類型或 `*`),若左側無實體則修飾左側類型。
| 陷阱 | 錯誤代碼 | 正確代碼 | 核心原因 |
|---|---|---|---|
| 野指針 | `int *p; *p = 1;` | `int *p = NULL; / malloc` | 未初始化指針指向隨機地址 |
| 懸空指針 | `free(p); *p = 1;` | `free(p); p = NULL;` | 釋放後指針仍指向已歸還記憶體 |
| 指針算術越界 | `int a[5]; int *p = a+10;` | `p < a+5` 檢查 | 指向陣列外記憶體,解引用 UB |
| 字串字面值修改 | `char *s = "hi"; s[0]='H';` | `char s[] = "hi"; s[0]='H';` | 字面值在唯讀段 |
| 函數返回區域指針 | `int* f() {int a; return &a;}` | 靜態/動態分配/輸出參數 | 區域變數生命週期結束 |
| const 指針賦值 | `const int *p = &a; *p = 1;` | 編譯錯誤 (正確阻止) | 底層 const 保護數據 |
| void* 算術 | `void *p; p++;` | `char *p; p++;` | void* 無大小,不可算術 |
| 格式化輸出指針 | `printf("%d", p);` | `printf("%p", (void*)p);` | `%p` 要求 void*,對應打印地址 |
總結: 指針是 C 語言直接操作記憶體的機制,掌握其本質是**理解記憶體佈局、生命週期、類型系統、編譯器行為**的鑰匙。從「指針就是地址」出發,透過**指針算術、解引用、類型修飾、多級間接、動態分配、函數回調**層層遞進,配合**const 正確性、錯誤防禦、工具輔助**,即可駕馭這把雙刃劍,寫出高效、健壯、優雅的系統級代碼。建議以「畫記憶體圖」作為理解複雜指針表達式的標準方法:方框代表記憶體單元,箭頭代表指針,標註類型、大小、地址、值,一圖勝千言。