1. 程式人生 > >C/C++ 中的算術及其陷阱

C/C++ 中的算術及其陷阱

[TOC] # 概述 無符號數和有符號數是通用的計算機概念,具體到程式語言上則各有各的不同,程式設計師是解決實際問題的,所以必須熟悉程式語言中的整數。C/C++ 有自己特殊的算術運算規則,如整型提升和尋常算術轉換,並且存在大量未定義行為,一不小心就會產生 bug,解決這些 bug 的最好方法就是熟悉整數性質以避免 bug。 我不是語言律師(非貶義),對 C/C++ 算術特性的瞭解主要來自教材和網際網路,但基本上都查閱 C/C++ 標準驗證過,C 和 C++ 在整數性質和算術運算上應該是完全相同的,如果有錯誤請指正。 # C/C++ 整數的陰暗角落 C/C++ 期望自己可以在所有機器上執行,因此不能在語言層面上把整數的編碼、性質、運算規定死,這讓 C/C++ 中存在許多未規定的陰暗角落和未定義行為。許多東西依賴於編譯器、作業系統和處理器,這裡通稱為執行平臺。 - 標準沒有規定整數的編碼,編碼方式依賴於執行平臺。 - `char`是否有符號依賴於執行平臺,編譯器有選項可以控制,如 GCC 的 -fsign-char。 - 移位大小必須小於整數寬度,否則是未定義行為。 - 無符號數左移 K 位結果為原來的 2^K 次方,右移 K 位結果為原來的數除 2^K 次方。僅允許對值非負的有符號數左移右移,運算結果同上,**對負數移位是未定義的**。 - 標準僅規定了標準內建整數型別(如`int`等)的最小寬度和大小關係(如`long`不能小於`int`),但未規定具體大小,如果要用固定大小的整數,請使用拓展整數型別(如`uint32_t`)等。 - 無符號數的溢位是合法的,**有符號數溢位是未定義行為** # 整型字面量 常常有人說 C/C++ 中的整數字面量型別是`int`,但這種說法是錯誤的。C/C++ 整形字面量究竟是什麼型別取決於字面量的格式和大小。StackOverflow 上有人問[為什麼在 C++ 中`(-2147483648> 0)`返回`true`](https://stackoverflow.com/questions/14695118/2147483648-0-returns-true-in-c),程式碼片段如下: ```c++ if (-2147483648 > 0) { std::cout << "true"; } else { std::cout << "false"; } ``` 現在讓我們來探索為什麼負數會大於 0。一眼看過去,`-2147483648`似乎是一個字面量(32 位有符號數的最小值),是一個合法的`int`型變數。但根據 C99 標準,字面量完全由十進位制(`1234`)、八進位制(`01234`)、十六進位制(`0x1234`)識別符號組成,因此可以認為**只有非負整數才是字面量**,負數是字面量的逆元。在上面的例子中,`2147483648`是字面量,`-2147483648`是字面量`2147483648`的逆元。 字面量的型別取決於字面量的格式和大小,C++11(N3337 2.14.2)規則如下: ![N3337 2.14.2](https://gitee.com/kongjun18/image/raw/master/Screenshot_20210331_105652.png) 對於十進位制字面量,編譯器自動在`int`、`long`、`long long`中查詢可以容納該字面量的最小型別,如果內建整型無法表示該值,在拓展整型中查詢能表示該值的最小型別;對於八進位制、十六進位制字面量,有符號整型無法表示時會選擇無符號型別。如果沒有足夠大的內建/拓展整型,程式是錯誤的,GCC/Clang 會發出警告。 在 C89/C++98 沒有`long long`和拓展整型,因此在查詢完`long`後查詢`unsigned long`。 現在再看上面的程式碼段就很清晰了,在 64 位機上,不論是 C89/C++98 還是 C99/C++11,都能找到容納該值的`long`型別(8 位元組),因此列印`false`。在 32 位機上,`long`佔據 4 個位元組無法容納字面量,在 C89/C++98 下,`2147483648`的型別為`unsigned long`,逆元`-2147483648`是一個正數(2^32 - 2147483648),列印`true`;在 C99/C++11 下,`2147483648`的型別為`long long`,逆元`-2147483648`是一個負數(-2147483648),列印`false`。 經過以上分析,可以判斷出提問者是在 32 位機上使用 C++98 做的實驗。 和字面量有關的另一個有意思的問題是`INT_MIN`的表示方法。《深入理解計算機系統(第 3 版)》2.2.6 中介紹的程式碼如下: ```c /* Minimum and maximum values a ‘signed int’ can hold. */ #define INT_MAX 2147483647 #define INT_MIN (-INT_MAX - 1) ``` 《深入理解計算機系統》沒有給出解釋,但這種寫法很顯然為了避免巨集`INT_MIN`被推導為`long long`(C99/C++11)或`unsigned long`(C89/C++98)。 # 整型提升與尋常算術轉換 再看一個 stackoverflow 上的提問[Implicit type promotion rules](https://stackoverflow.com/questions/46073295/implicit-type-promotion-rules),通過這個例子來了解 C/C++ 算術運算中的*整型提升*(*integer promotion*)和*尋常算術轉換*(*usual arithmetic conversion*)。提問者編寫了以下兩段程式碼,發現在第一段程式碼中,`(1 - 2) > 0`,而在第二段程式碼中`(1 - 2) < 0`。這種奇怪的現象就是整型提升和尋常算術轉換導致的。 ```c unsigned int a = 1; signed int b = -2; if(a + b > 0) puts("-1 is larger than 0"); // ============================================== unsigned short a = 1; signed short b = -2; if(a + b > 0) puts("-1 is larger than 0"); // will not print ``` 整型提升和尋常算術轉換涉及到整型的秩(優先順序),規則如下: - 所有有符號整型的優先順序都不同,即使寬度相同。 假如`int`和`short`寬度相同,但`int`的秩大於`short`。 - 有符號整型的秩大於寬度比它小的有符號整型的秩 `long long`寬度為 64 位元,`int`寬度為`32`位元,`long long`的秩更大 - `long long`的秩大於`long`,`long`的秩大於`int`,`int`的秩大於`signed char` - 無符號整型的秩等於對應的有符號整型 `unsigned int`的秩等於對應的`int` - `char`的秩等於`unsiged char`和`signed char` - 標準整型的秩大於等於對應寬度的拓展整型 - `_Bool`的秩小於**所有**標準整型 - 列舉型別的秩等於對應整型 上面的規則看似複雜,但其實就是說:內建整型是一等公民,拓展整型是二等公民,`_Bool`是弟弟,列舉等同於整型。 整型提升的定義如下: > C11 6.3.1.1 > > If an `int` can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an `int`; otherwise, it is converted to an `unsigned int`. These are called the *integer promotions*. 在算術運算中,秩小於等於`int`和`unsigned int`的整型(把它叫做小整型),如`char`、`_Bool`等轉換為`int`或`unsigned int`,如果`int`可以表示該型別的全部值,則轉換為`unsigned int`,否則轉換為`unsigned int`。由於在 x86 等平臺上,int 一定可以表示這些小整型的值,因此不論是有符號還是無符號,小整型都會隱式地轉換為 int,不存在例外(otherwise 所說的情況)。 在某些平臺上,`int`可能和`short`一樣寬。這種情況下,`int`無法表示`unsigned short`的全部值,所以`unsigned short`要提升為`unsigned int`。這也就是標準中說的*“否則,它將轉換為`unsigned int`”*。 ```c++ // C++17 // 有符號數溢位是未定義行為,但在許多編譯器上能看到正常的結果, // 這裡只是觀察現象,請不要認為有符號數溢位是合法的 #