段錯誤是 C 語言程式最常見的執行期崩潰。本文以一個經典的「陣列找終止標記」錯誤程式為例,逐層剖析三大核心死因:陣列參數退化為指標、迴圈缺乏邊界檢查、函式/變數命名衝突,並給出工業級修正版本。
int input(int a[]) { // 錯誤 1:陣列退化為指標,失去大小資訊
int i = 0, input = 0;
while (a[i] != -999) { // 錯誤 2:無邊界檢查,i 會衝出陣列
input++; i++;
}
return input;
}
int main(void) {
int value[101] = {0}; // 全 0 初始化,無 -999 終止標記
printf("%d\n", input(value)); // 必然越界訪問 → Segmentation Fault
return 0;
}
// 這兩個宣告完全等價
int input(int a[]);
int input(int *a); // 編譯器內部將前者轉為後者
// 後果:函式內 sizeof(a) 回傳指標大小 (4/8 bytes),而非陣列大小
解決:額外傳遞長度參數,或使用結構體封裝。
// 危險:while (a[i] != -999) — 陣列若無 -999,i 衝向無限大
// 安全:for (i = 0; i < len && a[i] != -999; i++)
int input(int a[]) { ... } // 函式名 input
int input = 0; // 區域變數名 input
// 某些編譯器/連結器會混淆,導致不可預期行為
#include <stdio.h>
#include <stddef.h> // size_t
// 回傳找到的元素個數,或 -1 表示未找到終止標記
int count_until_sentinel(const int *arr, size_t len, int sentinel) {
for (size_t i = 0; i < len; i++) {
if (arr[i] == sentinel) return (int)i;
}
return -1; // 未找到
}
int main(void) {
int value[101] = {0};
// 模擬資料:前 5 個有效,第 6 個為 -999
for (int i = 0; i < 5; i++) value[i] = i * 10;
value[5] = -999;
int count = count_until_sentinel(value, 101, -999);
if (count >= 0) {
printf("找到 %d 個有效元素 (索引 0-%d)\n", count, count - 1);
} else {
printf("陣列中無終止標記 -999\n");
}
return 0;
}
| 技巧 | 實現 | 適用場景 |
|---|---|---|
| 靜態斷言檢查陣列大小 | _Static_assert(N == 101, "Size mismatch"); | 編譯期保證大小一致 |
| 柔性陣列成員 | struct { size_t len; int data[]; } | 動態大小陣列封裝 |
| AddressSanitizer | gcc -fsanitize=address -g | 開發期精確定位越界 |
| Valgrind Memcheck | valgrind ./prog | Linux 下記憶體錯誤全景掃描 |
| 安全庫函式 | memcpy_s, strcpy_s (C11 Annex K) | 生產環境強制邊界檢查 |
gdb ./prog core → bt,定位崩潰行號。NULL?是否指向已釋放記憶體?是否越界?Segmentation Fault 不是隨機災難,而是記憶體存取契約違約的必然結果。將「陣列大小與指標同行」、「迴圈必有邊界」、「命名零衝突」內化為肌肉記憶,配合 AddressSanitizer 等現代工具鏈,能在編譯/測試階段消滅 99% 的段錯誤,讓 C 程式在生產環境穩如磐石。