C 語言以「靈活、高效、贴近硬件」著稱,但這份自由也帶來了諸多隱蔽的語義陷阱。本文聚焦 指針與數組、非數組指針使用、求值順序不確定性、整數溢出、高效二分查找、數組邊界處理 六大核心語義缺陷領域,結合代碼實例與底層原理,幫助開發者建立更穩健的 C 編程思維。
sizeof(數組) 獲取整體大小、&數組[0] 獲取首元素地址(隱式轉換為指針)。int a[3];
sizeof(a); // 12 (整個數組大小,非指針大小)
int calendar[12][31];
sizeof(calendar[4]); // 120 = 31 * sizeof(int),calendar[4] 是第 5 行(含 31 個 int)
calendar[1][2] 等價於 *(*(calendar + 1) + 2)
// calendar 退化為指向 [31]int 的指針 (*)[31]
// calendar + 1 跳過 1 行 → &calendar[1]
// *(calendar + 1) 解引用 → calendar[1] (類型 int[31])
// *(calendar + 1) + 2 → &calendar[1][2]
// 再次解引用 → calendar[1][2]
int calendar[12][31];
int **p = calendar; // ❌ 錯誤:類型不匹配
// calendar 類型:int (*)[31] (指向含 31 個 int 的數組的指針)
// p 類型:int** (指向 int* 的指針)
// 解引用時單位不同:p 以 sizeof(int*) 為步長,calendar 以 31*sizeof(int) 為步長
int (*p)[31] = calendar; // ✅ 正確:類型完全匹配
char calendar[12][31];
char **a = calendar; // ⚠️ 此處「碰巧」可用,但極度危險!
// 原因:字符串操作通常按「指針」處理 (如 printf("%s", a[0]))
// a[0] 類型 char*,printf 只讀取直到 '\0',不涉及 a[1][2] 這類二維索引
// 但若代碼後續出現 a[i][j] 訪問,立即崩潰或出錯
// 原則:嚴禁將二維數組賦值給雙重指針,除非極其確定僅作一維指針數組用
C 標準允許取 &array[N](N 為數組長度),即數組末尾後一個元素的地址。該地址可用於比較、賦值給指針,但絕不可解引用。常用於循環終止條件:
int calendar[12][31];
int (*monthp)[31];
for (monthp = calendar; monthp < &calendar[12]; ++monthp) {
int *dayp;
for (dayp = *monthp; dayp < &(*monthp)[31]; ++dayp)
*dayp = 0;
}
malloc 可能返回 NULL,必須判斷。\0 空間,strlen 不含結束符。free(ptr) 後立即 ptr = NULL,防野指針。char *p = NULL; // 地址為 0,不可解引用
char *q = ""; // 指向只讀區的一個 '\0',可安全打印
// *p → 崩潰;*q → '\0',安全
char *r, *s = "hi", *t = "man";
r = malloc(strlen(s) + strlen(t) + 1); // +1 留給 '\0'
if (!r) exit(1);
strcpy(r, s);
strcat(r, t);
printf("%s\n", r); // "himan"
free(r);
r = NULL;
y[i] = x[i++]; // ❌ 未定義行為
// i++ 何時生效?賦值前?賦值後?編譯器自主決定
// 可能結果:y[0]=x[0] 且 i=1;或 y[1]=x[0] 且 i=1
y[i] = x[i];
i++; // 明確分離副作用,行為確定
C 標準未規定函數參數、運算子操作數的求值順序(除 &&、||、?:、, 外)。同一序列點內對同一標量對象多次修改、或修改後讀取(非為了確定新值),均屬未定義行為。
#include
// 方法 1:轉無符號比較(利用無符號環狀特性)
if ((unsigned)a + (unsigned)b > INT_MAX) { /* overflow */ }
// 方法 2:利用邊界值反推(推薦,無類型轉換風險)
if (a > INT_MAX - b) { /* overflow */ }
// 同理減法:if (a < INT_MIN - b) { /* underflow */ }
-INT_MIN 會溢出(因 |INT_MIN| = INT_MAX + 1),唯一無法安全取負的值。處理絕對值時必須特判。
(hi - lo) >> 1 等價 (hi - lo) / 2,但指令級更快。mid = (lo + hi) / 2 錯誤,指針相加無意義;只能 lo + (hi - lo) / 2。int *bsearch(int *t, int n, int x) {
int *lo = t, *hi = t + n - 1;
while (lo <= hi) {
int *mid = lo + ((hi - lo) >> 1);
if (x < *mid) hi = mid - 1;
else if (x > *mid) lo = mid + 1;
else return mid;
}
return NULL;
}
上界 - 下界。上界 == 下界。
void bufwrite(char *p, int n) {
while (--n >= 0) {
if (bufptr == &buffer[N]) // 觸及右邊界(end)
flush();
*bufptr++ = *p++;
}
}
// 任意時刻已存字符數 = bufptr - buffer (指針相減得元素個數)
// bufptr == &buffer[0] → 緩衝區為空
C 語言的語義陷阱本質上源於抽象層級較低、信任程序員、編譯器優化激進三大特性。掌握這六大領域的核心規則,配合 -Wall -Wextra -Werror -fsanitize=undefined 等編譯器工具鏈,才能在享受 C 語言高性能的同時,構建健壯、安全、可維護的系統級代碼。