現象重現
int m = 1;
int i = 0;
int k[10] = {0};
printf("&m = %p\n", &m);
printf("&i = %p\n", &i);
printf("&k[0] = %p\n", &k[0]);
printf("&k[9] = %p\n", &k[9]);
printf("&k[10] = %p\n", &k[10]); // 越界訪問!
for (; i <= 10; ++i) {
k[i] = 0; // 當 i=10 時,寫入 k[10] 覆蓋了變數 i
}
printf("HelloWorld!\n"); // 永遠不會執行,陷入死循環
記憶體佈局圭示 (典型 x86 釋出構建)
高地址
+------------------+
| main 參數/返回地址 |
+------------------+
| 儲存的 EBP | ← EBP
+------------------+
| k[0] ~ k[9] | 40 bytes (10 * 4)
| k[10] (越界) | ← 這裡剛好覆蓋 i 的位置!
+------------------+
| i (4 bytes) | ← 被 k[10] 覆蓋
+------------------+
| m (4 bytes) |
+------------------+
低地址 (棧增長方向 ↓)
為什麼會這樣排列?
- 區域變數在棧上分配,編譯器決定相對位移
- 聲明順序不等於記憶體順序,編譯器可自由重排以滿足對齊
- 對齊需求:
int[10] 可能按 16 位元組對齊,導致後面有填充位元組
- 編譯器保護機制:部分編譯器 (MSVC / GCC -fstack-protector) 故意在陣列後留出「紅區」檢測越界
不同編譯器行為差異
| 編譯器/選項 | k[10] 位置 | 結果 |
| GCC -O0 (預設) | 緊接 k[9] 後 | 高概率覆蓋 i → 死循環 |
| GCC -O2 | 可能優化掉整個循環 | 行為不可預測 |
| MSVC /RTCs (檢查) | 插入填充/金絲雀值 | 運行時斷言失敗 |
| Clang -fsanitize=address | 紅區保護 | 即時報錯: stack-buffer-overflow |
標準規定:未定義行為 (UB)
C 標準 (C99 §6.5.6/8) 明確規定:**陣列越界訪問屬於未定義行為**。編譯器可以:
- 產生看似「正常」但邏輯錯誤的程式碼 (如本例)
- 直接崩潰
- 優化時假設不會越界,導致邏輯錯誤
- 任何其他行為
如何檢測與預防
// 1. 靜態分析
// GCC/Clang: -Wall -Warray-bounds
// MSVC: /Wall (較吵) 或 /analyze
// 專用工具: cppcheck, clang-tidy, Coverity
// 2. 執行期檢測
// GCC/Clang: -fsanitize=address -fno-omit-frame-pointer
// MSVC: /RTCs (Debug 構建)
// 3. 程式碼層面防禦
#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof((arr)[0]))
for (i = 0; i < ARRAY_SIZE(k); ++i) { // 嚴格 < 而非 <=
k[i] = 0;
}
// 4. C++ 現代化替代
std::array k = {0}; // .at(10) 會拋出 out_of_range
// 或
std::vector k(10);
經典面試延伸題
「如果將 int k[10] 改為 char k[10],會發生什麼?」
答:char 1 位元組,k[10] 偏移 10 位元組。若 i 在 4 位元組對齊地址,可能不會被覆蓋,但仍屬 UB,行為不可靠。
總結
這個「經典面試題」實際上展示的是 **C 語言不做邊界檢查** 與 **棧幀佈局由編譯器決定** 的特性。**永遠不要依賴越界訪問的具體表現**,而應用工具與規範在編譯/執行期捕獲它們。
- 上一篇文章: DevExpress ASPxPageControl 動態添加 TabPage 含 iframe 完整解決方案
- 下一篇文章: C# GDI+ 繪圖出現黑色邊框:畫筆寬度導致矩形填滿畫布
Copyright ©
程式碼之家 All Rights Reserved