C語言設計哲學:**程序員知道自己在做什麼**。編譯器**不檢查**數組越界、空指針解引用、有符號溢出、類型懲罰、參數匹配 (可變參數)、序列點違規等。**這些錯誤編譯通過,運行時可能正常、可能崩潰、可能沉默產生錯誤結果、可能被優化器「優化掉」關鍵邏輯**。本文系統梳理高頻錯誤,提供檢測/修復方案。
int a = 5; printf("%d", A); // 錯誤:未聲明標識符 A (大寫)
規範:變量/函數小寫蛇形 snake_case,常量/宏全大寫 UPPER_SNAKE,類型/結構體帕斯卡 PascalCase (C++常用)。
float a, b; printf("%d", a % b); // 錯誤:% 運算符要求整數操作數
修正:用 fmod(a, b) (<math.h>) 或轉整數 (注意範圍/精度損失)。
char c; c = "a"; // 錯誤:字符串字面量類型 const char[2] (含\0),不可賦給 char
c = 'a'; // 正確:字符字面量類型 int (C) / char (C++),值為 ASCII 碼
= (賦值) vs == (比較)if (a = 3) ... // 警告:賦值結果作為條件 (非零為真),極易寫錯
if (a == 3) ... // 正確
// 防禦性寫法:if (3 == a) ... (常量在左,誤寫 = 會編譯報錯)
a = 1
b = 2; // 錯誤:上行漏分號,編譯器合併兩行 → 語法錯誤
for (i=0; i<10; i++); // 陷阱:分號構成空語句循環體,下面塊只執行一次
{ scanf(...); }
規範:複合語句 {...} 末尾**不加分號**;if/for/while 後**不加分號** (除非故意空體並加註釋)。
scanf 缺地址運算符int a; scanf("%d", a); // 錯誤:傳值,scanf 寫入地址 a 解釋為地址 → 崩潰/亂寫內存
scanf("%d", &a); // 正確
scanf 輸入分隔符scanf("%d%d", &a, &b); // 輸入必須用空白 (空格/換行/Tab) 分隔,**不能用逗號**
// 輸入 "3,4" → 讀取 3 到 a,遇到 ',' 匹配失敗,b 不變,',' 殘留緩衝區
switch 缺 break (貫穿)switch(x) { case 1: foo(); case 2: bar(); } // x=1 時 foo() 和 bar() 都執行
// 故意貫穿需註釋 /* fallthrough */ (C++17 [[fallthrough]])
if-else 懸空 else
if (a) if (b) foo(); else bar(); // else 匹配最近的 if (b)
// 加大括號消除歧義:
if (a) { if (b) foo(); else bar(); }
for (i=0; i<10; i++) { if (cond) i++; } // 體內修改循環變量 → 跳過迭代/死循環
while (cond) { /* 忘記更新 cond 相關變量 */ } // 死循環
int *p; *p = 10; // 未初始化,指向隨機地址 → 崩潰/靜默破壞
int *q = malloc(4); free(q); *q = 20; // use-after-free
int *r = foo(); // 返回局部數組/變量地址 → 棧幀銷毀後指針失效
// 鐵律:聲明即初始化 (NULL/合法地址),free 即置 NULL,不返回棧地址
char buf[10]; strcpy(buf, "hello world"); // 寫入 12 字節 (含\0) → 溢出 2 字節
// 後果:覆蓋相鄰變量、破壞棧幀返回地址 (控制流劫持)、觸發棧保護機制崩潰
// 修正:strncpy/strlcpy/snprintf,或 C++ string/span
void foo() { int *p = malloc(100); if (err) return; /* 漏掉 free(p) */ }
// 長期運行程序必然 OOM。工具:Valgrind, ASan, mtrace, 靜態分析。
// C++: RAII (unique_ptr/vector) 自動管理。
free(p); free(p); // 堆元數據結構破壞 → 後續 malloc/crash 不可預測
// 修正:free 後 p = NULL; free(NULL) 安全無操作。
float f = 1.0f; int i = *(int*)&f; // 違反嚴格別名規則 → 編譯器優化假設失效 → UB
// 合規:union {float f; int i;} u; u.f = 1.0f; i = u.i; // C99+ 保證
// 或 memcpy(&i, &f, sizeof f); // 字節拷貝,無 UB
int a[10]; int *p = a; p += 20; *p = 0; // 指向數組對象之外 (含尾後一個) → UB
// 只能指向數組元素或尾後一個位置 (用於迭代器終止判斷),解引用尾後 UB。
char s[5] = {'h','e','l','l','o'}; // 無 \0
printf("%s", s); // 繼續讀取後續內存直到遇到 \0 → 洩漏/崩潰
// 初始化:char s[] = "hello"; 或 char s[6] = "hello"; 確保空間含 \0
gets / scanf("%s") 無長度限制
gets(buf); // 已從 C11 標準移除,極度危險
scanf("%s", buf); // 無寬度限制,輸入超長 → 溢出
// 替代:fgets(buf, sizeof buf, stdin); 或 scanf("%99s", buf); (寬度 = size-1)
fopen("data.bin", "r"); // 文本模式:讀取時 \r\n → \n,寫入時 \n → \r\n,EOF (Ctrl+Z) 截斷
fopen("data.bin", "rb"); // 二進制模式:字節流無轉換,**處理二進制數據必須用 "b"**
UB 意味著標準對程序行為無任何約束:可能崩潰、可能輸出任意值、可能刪除硬盤、**可能被優化器假設不發生而刪除檢查代碼**。
int x = INT_MAX; x++; (無符號溢出定義為模 2^n)。int *p = NULL; *p = 0;。1 << 32 (int 32位)、-1 >> 1 (負數右移實現定義但不可移植)。i = i++ + ++i;、a[i] = i++; (C++17 前)。int x = 1/0;。printf("%d", 1.0f))。-Wall -Wextra -Wpedantic -Werror -Wshadow -Wconversion -Wsign-conversion -Wnull-dereference -Wdouble-promotion -Wformat=2。-fsanitize=address (ASan):堆/棧/全局緩衝區溢出、UAF、雙重釋放。-fsanitize=undefined (UBSan):有符號溢出、除零、空指針解引用、移位溢出、對齊、枚舉範圍、浮點轉換、vla bound 等 **100+ 種 UB**。-fsanitize=memory (MSan):未初始化內存讀取 (需全程插桩)。-fsanitize=thread (TSan):數據競爭。-fsanitize=leak (LSan):內存洩漏 (ASan 內建)。scan-build)、Cppcheck、Coverity、PVS-Studio、SonarQube。-Werror 強制。std::vector/string/unique_ptr/shared_ptr/span/optional/variant 消除 90% 手動內存管理。_Nonnull/_Nullable (Clang)、__attribute__((malloc))、cleanup 屬性模擬 RAII。assert (Debug)、if (idx >= len) return ERR; (Release)、安全庫函數 (strncpy_s Annex K / strlcpy BSD / snprintf)。assert(ptr != NULL); assert(len > 0); assert(is_sorted(arr, n)); 文檔化前置/後置條件。const 變量/指針/成員函數/引用,**傳播不可變性,編譯器優化更強,Bug 更少**。enum class) 替代宏/裸整數,消除隱式轉換/命名衝突。C語言錯誤多源於**「以為編譯器會管」的錯覺**。**編譯器只管語法/類型/調用約定,不管語義/邏輯/運行時安全**。建立**「UB 無處不在、Sanitizer 必須開啟、警告即錯誤、測試覆蓋邊界、靜態分析常態化」**的工程文化,才能在 C/C++ 的自由中馭馭危機,構建可靠系統。