未定義行為與安全

差一錯誤與柵欄柱錯誤

這裡有個幾乎人人都會中招的謎題:要蓋一道筆直、長 10 公尺、每隔 1 公尺立一根柱子的柵欄,你需要幾根柱子?誘人的答案是 10。正確答案是 11——兩端各一根,加上中間九根。因為把「間隔」和「柱子」搞混而剛好少算一個,這種錯太常見,以至於有了名字:柵欄柱錯誤(fencepost error),它在程式設計裡的表親就是差一錯誤(off-by-one error)。它是所有程式設計中最普通、最頻繁的臭蟲,而在 C 裡它常常變成未定義行為。

這個錯誤就是用了一個高一或低一的計數:迴圈多跑一次、少配置一個位元組、索引超過尾端一格。C 讓人很容易犯,因為陣列從零起算:n 個元素的陣列索引是 0 到 n-1,所以最後一個有效索引是 n-1,「不是」n。兩種經典形式:寫成 for (i = 0; i <= n; i++) 的迴圈會跑 n+1 次、碰到越界的 a[n];以及忘了 C 字串需要「多一個」位元組放結尾的空字元,所以 5 個字元的字串需要 6 位元組的緩衝區,而剛好配置 5 個就沒地方放 '\0' 了。差一錯誤也是那個危險情形的主因:在緩衝區外多寫一個元素就是緩衝區溢位,而那單獨一個跑掉的位元組就能毀掉相鄰物或砸毀返回位址。

為什麼重要:差一錯誤容易犯也容易看漏,因為程式通常「差一點」就對了——它把中間處理得好好的,只在最頭或最尾那個元素絆倒。兩個習慣能馴服它。第一,優先用半開區間:把迴圈寫成 0 <= i < n(用 < 而非 <=),這正是為什麼 a[0..n-1] 和 'for (i = 0; i < n; i++)' 配得這麼乾淨。第二,為字串估算緩衝區大小時,永遠多加一個給結尾的空字元、並在程式碼裡明說(malloc(len + 1))。把邊界情形畫在紙上——看 i = 0 和 i = n - 1 各會發生什麼——能在它們出貨前抓到大多數。

/* 差一迴圈:碰到越界的 a[n] */ for (int i = 0; i <= n; i++) sum += a[i]; /* 應為 i < n */ /* 差一配置:沒地方放 '\0' */ char *s = malloc(strlen(src)); /* 需要 strlen(src) + 1 */ strcpy(s, src); /* 多溢出一個位元組 */ char *ok = malloc(strlen(src) + 1); /* +1 給結尾字元 */

兩個日常的差一:用 <= 而非 < 走過陣列尾端;忘了給結尾字元 +1 會讓緩衝區多溢出一個位元組。

n 個元素的陣列有效索引是 0..n-1,所以最後一個索引是 n-1、不是 n。優先用半開區間 'i < n',並且永遠為 C 字串配置 strlen + 1 個位元組以容納結尾字元。

又称
off-by-onefencepost errorOBOE差一錯誤柵欄柱問題