標籤:

為什麼printf("%ld ", -2147483648/1000000000);得到的結果是2而不是-2?

printf("%ld
", -2147483648/1000000000);

得到的結果是2而不是-2

但是

int a = -2147483648;

int b = 1000000000;

printf("%d
", a/b);

的結果是-2。

使用gcc (Debian 4.7.2-5) 4.7.2

編譯時提示

warning: this decimal constant is unsigned only in ISO C90 [enabled by default]


對這類問題,以及類似的問題,我有這幾個建議:

1,認真看 warning 信息。

2,學好英語,至少確保足夠的閱讀能力(聽、說的能力對程序員來說遠遠沒有閱讀能力重要)

3,學好 google 的使用,至少學會「拿 warning 信息當作關鍵詞去搜索」這個技能。

其它該講的,其它樓層也都講了。

補充:

這個問題實際上警告的是 C90 標準對整數常量的特別對待,會造成意料之外的結果。

要解決這個問題最簡單的方案其實是:把編譯器標準換成 C99,或者 C1x

一般來說,我認為新項目可以忽略 C90 這種 二十多年前的老標準。

使用 gcc -std=c99 或者 gcc -std=c1x 都不會存在這個問題。


很有趣的問題。

-2147483648並不是一個數值常量,而是一個表達式,它是2147483648這個數值常量前面加了一個單目運算符「-」。

而2147483648本身超出了int型的範圍,而你又沒有用後綴(L,UL等)指明它的類型,所以會導致undefined behavior。

下面是我想到的,-2147483648這個表達式的幾種可能的求值過程:

1) 2147483648超出了int型的範圍,溢出後變成了int型的-2147483648。對這個值求相反數,結果2147483648又超出了int型的範圍,又溢出變成了int型的-2147483648。結果恰好對了。

2) 2147483648超出了int型的範圍,被cast成了unsigned int型的2147483648。對這個值求相反數,結果-2147483648又超出了unsigned int型的範圍,又溢出變成了unsigned int型的2147483648。結果差了一個負號。

3) 2147483648超出了int型的範圍,被cast成了long int型(64位)的2147483648。對這個值求相反數,結果-2147483648仍在long int型的範圍內。結果恰好對了。

上面3種情況都只是「可能」的,具體是哪一種依賴於編譯器。作為程序員,不應該依賴這種undefined behavior。

再回到你的程序。

4) int a = -2147483648; 這一句,無論-2147483648經過了上面3種求值過程中的哪一種,最後的結果都會被cast成int型賦給a。這樣,a的值一定是-2147483648。所以a/b的計算結果是對的。

5) 而printf("%ld
", -2147483648/1000000000); 這一句,從你的編譯器輸出(enabled)來看,應該是把2147483648當成了unsigned int型常量,即採用了上面第2種求值過程。於是結果就反了。

Bonus:

既然-2147483648這個表達式會導致undefined behavior,那麼當程序中真的要用到-2147483648這個值的時候,應該怎麼辦呢?有兩種辦法:

a) 寫成(int)-2147483648。由上面(4)的分析,cast成int型之後值一定是負的。不過,這樣做雖然結果正確,但過程中畢竟引發了undefined behavior,並不推薦。

b) 使用INT_MIN。這個宏的定義如下:

#define INT_MIN (-2147483647 - 1)

這個表達式不會引發undefined behavior,且結果確實是-2147483648。

參考:

(-2147483648&> 0) returns true in C++?

Why it is different between -2147483648 and (int)-2147483648


-2147483648/1000000000出錯的原因是 / 左邊得到的是無符號(負)數,/ 右邊是有符號數。

1. 把你的程序中%ld改為%d,然後編譯運行。 -std=c89 是默認值,不寫也可以,-m32表示生成32位機器的代碼,我的系統是64位的, long int 的最大值大於214748367。根據warning信息,可以知道 -2147483648/1000000000 經編譯器評估後類型是 long unsigned int 而不是 signed int。據此推測 -2147483648經編譯器評估後的類型為 long unsigned int

? /tmp cat -n c.c
1 #include &
2 int main(){
3 printf("%d
", -2147483648/1000000000);
4 }
? /tmp gcc -m32 -std=c89 c.c
c.c: In function 『main』:
c.c:3:2: warning: this decimal constant is unsigned only in ISO C90 [enabled by default]
c.c:3:2: warning: format 『%d』 expects argument of type 『int』, but argument 2 has type 『long unsigned int』 [-Wformat]
? /tmp ./a.out
2
? /tmp

2. 把 -2147483648 賦給long signed 類型的 s 後,s 的值變為2147483648。所以 -2147483648/1000000000 的值是2。把 s 賦給long unsigned 類型的 aa 後,只要按照正確的方式解讀 aa 變數中的二進位數,就能輸出正確的值(aa(lu) 和 s(lu) 相同)。這涉及到數的二進位表示,正負數的原碼補碼錶示,這些我不熟,想了解的自己查wikipedia吧。

? /tmp cat -n unsigned.c
1 #include &
2
3 int main(){
4 long unsigned a = -2147483648/1000000000;
5 long signed s = -2147483648;
6 long unsigned aa = s;
7 printf("a(lu) = %lu
", a);
8 printf("aa(ld) = %ld, aa(lu) = %lu, s(lu) = %lu
", aa, aa, s);
9 }
? /tmp gcc -m32 -std=c89 unsigned.c
unsigned.c: In function 『main』:
unsigned.c:4:2: warning: this decimal constant is unsigned only in ISO C90 [enabled by default]
unsigned.c:5:2: warning: this decimal constant is unsigned only in ISO C90 [enabled by default]
? /tmp ./a.out
a(lu) = 2
aa(ld) = -2147483648, aa(lu) = 2147483648, s(lu) = 2147483648
? /tmp

3. 為了驗證這個想法,把 1. 的printf 改成

printf("%ld
", ((long signed)-2147483648)/1000000000);

再編譯,就會得到正確的 -2 了。

C90 標準中說,沒有後綴的數學常量的類型依次 int, long int, unsigned long int,直到這個類型可以表示這個數。C99 的類型列表則是 int, long int, long long int,所以上面的代碼換成 -std=c99 也沒有問題。至於為什麼 -2147483648 在C90里會被評估為 unsigned long int,根據這裡 Warning: this decimal constant is unsigned only in ISO C90 ,是因為編譯過程中 -2147483648由 2147483648 取反得到,而 2147483648 超出了 int, long int的範圍,所以被提升為 unsigned long int(你的機器上int 和 long int的範圍是一樣的),這也解釋了 warning: this decimal constant is unsigned only in ISO C90。

根據 Richard Earnshaw ,C90隻有正的常量,負的常量通過正的常量取反得到。所以在這個過程中-2147483648的類型就要被提升為unsigned long int了。

另一個參考:Can"t get rid of "this decimal constant is unsigned only in ISO C90" warning

這兩個 stackoverflow 的鏈接是 google "warning: this decimal constant is unsigned only in ISO C90" 搜索結果的前兩條。善用google,而且一定使用英文的錯誤提示作為關鍵詞,這樣才能搜索到英文的內容,英文世界比中文世界豐富多了。


跑題一下…

解決一個編譯期錯誤,比解決一個運行時錯誤成本要低的多。

很多編譯警告,將來可能就會成為一個運行時錯誤。連編譯警告都不認真對付的人,很難相信他會對自己的代碼仔細審查。

一直不明白怎麼這麼多人不拿警告當回事…

大概是因為徹底了解一個警告的含義,需要很多背景知識。既然編譯器都認為這是一個合乎語法但存在其他隱患的點,那麼它往往涉及了一些模糊的灰色地帶。利用這個機會懂得更多細節,真是個上佳的學習機會。

關於計算機系統內部數的表達方式,尤其是浮點,我覺得最好的書是《深入理解計算機系統》,中譯本


可能是%ld用的不對吧,我不知道你怎麼編譯成功的,我 gcc 4.8.1 clang 3.2 ubuntu 系統


推薦閱讀:

在計算機語言發展歷史上,C語言和C++語言分別有怎樣的歷史意義?
為什麼C語言中2個無符號數相減會得到負數?
c++中有些重載運算符為什麼要返回引用?
為什麼C語言考試不夠好?

TAG:C編程語言 |