無號環繞與有號溢位的對比
這個條目之所以存在,是因為混淆兩個表面相似的東西——無號算術的環繞和有號算術的溢位——是 C 程式設計師可能犯下最有後果的錯誤之一。在硬體上它們常看來一模一樣:位元以相同方式翻轉。但「語言」把它們當成日與夜,而這個差別可能決定你的程式是正確、還是悄悄壞掉。
無號環繞是完全「有定義」的。C 標準說無號算術是模算術:結果對 2^n 取模,就這樣。所以 0u - 1u 正好是 UINT_MAX,而 UINT_MAX + 1u 正好是 0——你可以依賴它、在它之上建立雜湊和環形緩衝區,最佳化器也必須遵守它。相對地,有號溢位是「未定義行為」。當一個有號計算超出範圍時,標準完全「不施加任何要求」。這不代表「它會環繞」,也不代表「它是實作定義的」——它代表編譯器被允許假設有號溢位根本從不發生,並依此假設進行最佳化。
實務後果尖銳而常令人意外。因為編譯器「可以假設」沒有有號溢位,它能合法地把「x + 1 > x」重寫成「true」,並刪掉你寫的安全檢查,任由溢位破壞下游的資料。同樣的原始碼若用無號型別寫,就會安全且可預測。由此而來的紀律是:當你真的想要模算術環繞時用無號型別;對有號值,根本別讓溢位發生——事先檢查範圍、加寬型別,或使用檢查算術的輔助函式。把「反正硬體會環繞」當成陷阱,而非保證。
unsigned u = 0; u--; 得到 UINT_MAX(有定義、安全)。int x = INT_MAX; x++; 是有號溢位(未定義)——編譯器可以假設它從不發生,並悄悄錯誤編譯周圍的程式碼。
看似相同的翻轉;一個是法則,另一個是未定義行為。
在一個運算式裡混用有號與無號會觸發轉換,常把有號值變成無號,所以「-1 < 1u」是「假」——那個 -1 變成了 UINT_MAX。用 -Wsign-conversion 編譯以捕捉這些意外。