未定義行為與安全

有號數溢位作為未定義行為

每個固定大小的數字都有它能容納的最大值,就像汽車里程表最多到 999999。叫它再往上一格,在硬體上通常會「環繞」回最小值。你可能合理地期待 C 也這麼做。對「無號」整數而言確實如此,而且這個環繞是良好定義的。但對「有號」整數來說,超過最大值是未定義行為——這個令人意外的不對稱,連有經驗的程式設計師都會踩到。

具體來說:32 位元的 int 可容納到 INT_MAX,也就是 0x7fffffff,即 2147483647。在 C 裡計算 INT_MAX + 1 是未定義行為。不是「它會環繞到 INT_MIN」——那是典型二補數硬體會做的事,但 C 標準並未承諾它,最佳化器也可以自由假設這個溢位永不發生。相對地,無號 int 計算 UINT_MAX + 1 則被「定義」為環繞回 0;用標準自己的話說,無號運算就是模數運算。所以同一個位元層級的操作,對一種型別是未定義、對另一種卻完全有定義。標準把有號溢位留為未定義,一部分是歷史因素(早期機器使用不同的有號表示法),一部分是為了授權某些最佳化,例如假設迴圈計數器 i 只會遞增、因而迴圈必定會終止。

為什麼重要:因為編譯器可以假設有號溢位永不發生,最直覺的溢位檢查就壞了。寫 if (x + y < x) 來偵測兩個 int 相加溢位,可能被整個最佳化掉,因為「x + y < x」蘊含溢位、而溢位「不可能發生」。正確作法是:在無號型別裡做運算或檢查、在相加「之前」就拿極限值比較(if (x > INT_MAX - y) ...)、或使用像 __builtin_add_overflow 這種能安全回報溢位的編譯器內建函式。永遠不要靠想像硬體會做什麼來推理有號溢位——在 C 裡,硬體的環繞不是你可以倚賴的保證。

int x = INT_MAX; int y = x + 1; /* UB:不要假設這會是 INT_MIN */ /* 壞掉的檢查:可能被最佳化成 'if (false)' */ if (a + b < a) report_overflow(); /* 安全的檢查:在相加前先比較 */ if (b > 0 && a > INT_MAX - b) report_overflow(); unsigned u = UINT_MAX; u = u + 1; /* 有定義:環繞回 0(模數) */

有號溢位是 UB,其天真的檢查可能被刪除;無號溢位是良好定義的環繞。請在操作前先檢查有號的極限值。

有號溢位=未定義行為;無號溢位=有定義的環繞(對 2 的 N 次方取模)。把兩者混為一談是經典臭蟲。有號型別在硬體上的環繞並非標準的承諾,所以最佳化器可以無視它。

又称
signed overflow有號整數溢位