C語言兩數定義正確,相乘溢出的原因?

gcc 4.9.2版本,int是32位的,long是64位的:

long a = 111111 * 111111;

printf("%ld
", a);

編譯時提示警告,說整型溢出。

改成:

int b = 111111;

long a = b * b;

編譯時不提示溢出警告,但輸出結果溢出,輸出 -539247567。

再改成:

long b = 111111;

long a = b * b;

結果就沒有問題了,正確輸出12345654321。

在xcode里情況也一樣。變數的定義都沒問題,但運算就出問題,這種情況是什麼原因?除了乘法,其他運算有類似情況嗎?


11111本身沒有超過int的範圍,所以11111的類型是int

int*int = int,這時已經溢出了

你用誰接收這個結果都沒用,至於編譯器有沒有提示,這是編譯器的問題,編譯器沒有義務提供標準以外的任何信息,這頂多算個警告

你可以手動指定11111的類型為11111UL或者11111LL,這樣他們乘出來就不會溢出了

順便說32位平台int和long一樣長


好有意思的問題!我記得以前的編譯器是不會出warning的,所以特意試了下。

------------------------------------------------------------------------------

1. GCC 4.8.5 和Visual Studio 2013

語句: long a = 111111 * 111111;

編譯結果:

GCC 4.8.5 : Warning:整數溢出

VC: 整型常量溢出

語句改為

long a = 111111 * 111;

編譯結果:GCC和VC均無錯誤,無警告。

2. GCC 3.2 (Dev-CPP)

同樣的語句: long a = 111111 * 111111;

無錯誤,無警告。

為什麼呢? 因為編譯器看到像 111111 * 111111這樣完全由常量組成的表達式就會想:尼瑪搞這麼麻煩幹什麼,我幫你算一下!這個表達式的值會在編譯階段就被計算並將計算結果存儲在程序代碼中。

實驗表明:在比較新的編譯器上,對完全由常量組成的表達式111111 * 111111和111111 * 111在編譯階段就進行算術運算並判斷其結果是否溢出,而比較舊的的編譯器默認沒有這麼做。

(誰能告訴我這是不是C11的新特性?)

第二個問題是——為什麼下面的表達式會溢出:

long a = 111111 * 111111;

溢出的是表達式 111111 * 111111,跟左邊的long a沒有一毛錢關係,但是long a會迷惑你。

表達式中兩個111111默認地被編譯器當做int進行處理,那麼這個表達式的結果類型也被預設為int,而111111 * 111111被新的編譯器判定超出了int範圍,所以提示溢出。

改成 @大荃 說的

long a = 111111L * 111111L;

或者long a=(long)111111*111111即可。

因為這次表達式中至少有一個操作數被顯式轉換為long型,所以表達式的結果類型也是long, 不會溢出。

第三個問題是——為什麼long a= b * b沒有提示溢出

因為b是變數,編譯器懶得檢查。


C 是弱類型的。

int a;
long b;
b = a*a;

最後一句等效為 b = (long)(a * a)。類型轉換是在賦值之前完成的。

如果你寫成 b = (long)a * a 就沒問題了。


這個涉及到C語言中的類型提升問題。

語句1: long a = 111 111 * 111 111

語句2: int b = 111 111

long a = b * b

語句3: long b = 111 111

long a = b * b

語句1和語句2其實是等價吧。計算機在執行語句1時,把表達式111 111*111 111中的111 111當作int 類型來處理。

至於語句1報出溢出錯,而語句2沒報錯,這是編譯器的原因了。但語句2輸出的答案卻不是正確的。這是因為語句2定義b 是int 類型,而111 111 *111 111等於12345654321對應的二進位是10 1101 1111 11011011 1011 1100 0011 0001,問題就出在這裡,int b是32位,但是正確結果需要34位來表示,所以在計算機內存儲的計算結果沒有高兩位。這時候需要將結果強制轉換為long a ,需要64位。這時候就要類型提升。這時候計算機內存儲的long a為1111 1111 1111 11111111 1111 1111 11111101 1111 1101 10111011 1100 0011 0001,然後輸出就變成了-539247567 。

至於語句3,因為定義的時候就把b定義成long 型,也就沒有後面的類型提升的問題,也就不會出錯,輸出來的答案就是正確的啦。

第一次答題,不知道有沒有說清楚。還望多指教哈:)

補充一點,整形數據在計算機中都是以二進位的形式存儲,不管是有符號還是無符號,負數是以補碼存在,正數為原碼,強制類型轉換其實就是位數的改變。當需要輸出時,計算機就按不同的類型解釋那個數據輸出。

至於float 型,double 型在計算機中是以IEE754標準存儲的。


看看彙編碼分析下到底發生什麼了吧

=======================分隔線========================

由於win32上long也被認為是int32_t的類型,因此使用了int64_t的原型long long並且相應的轉到了iostream進行輸出

首先是第一段

#include &

int main(){

long long a = 111111*111111;

std::cout&<&

對應的彙編代碼

call ___main
movl $-539247567, -16(%ebp)
movl $-1, -12(%ebp)
movl -16(%ebp), %eax
movl -12(%ebp), %edx
movl %eax, (%esp)
movl %edx, 4(%esp)
movl $__ZSt4cout, %ecx

=======================分隔線========================

然後再來看第二段代碼對應的彙編

#include &

int main(){

int b = 111111;

long long a = b*b;

std::cout&<&

call ___main
movl $111111, -12(%ebp)
movl -12(%ebp), %eax
imull -12(%ebp), %eax
cltd
movl %eax, -24(%ebp)
movl %edx, -20(%ebp)
movl -24(%ebp), %eax
movl -20(%ebp), %edx
movl %eax, (%esp)
movl %edx, 4(%esp)
movl $__ZSt4cout, %ecx

=======================分隔線========================

再來看下第三個

#include &

int main(){
long long b = 111111;

long long a = b * b;

std::cout&<&

call ___main
movl $111111, -16(%ebp)
movl $0, -12(%ebp)
movl -12(%ebp), %eax
imull -16(%ebp), %eax
movl %eax, %edx
movl -12(%ebp), %eax
imull -16(%ebp), %eax
leal (%edx,%eax), %ecx
movl -16(%ebp), %eax
mull -16(%ebp)
addl %edx, %ecx
movl %ecx, %edx
movl %eax, -24(%ebp)
movl %edx, -20(%ebp)
movl %eax, -24(%ebp)
movl %edx, -20(%ebp)
movl -24(%ebp), %eax
movl -20(%ebp), %edx
movl %eax, (%esp)
movl %edx, 4(%esp)
movl $__ZSt4cout, %ecx

我覺得從彙編上可以挺明顯的看出三者之間的區別的:

對於第一段代碼,在沒有聲明111111L的情況下,編譯器在優化階段直接把111111*111111作為const進行了優化,並且是以int的形式,因此形成了int*int的溢出,同時由於是在編譯階段,因此編譯器可以正確的Warning

對於第二段代碼,可以看到,

movl $111111, -12(%ebp)
movl -12(%ebp), %eax
imull -12(%ebp), %eax

計算是發生在類型轉換之前,也就是先進行了int*int的計算,然後才是類型轉換,同時也符合C/C++在runtime階段不會在程序員未要求的情況下進行變數檢查和溢出檢查的特性

再看第三段

movl $111111, -16(%ebp)
movl $0, -12(%ebp)
movl -12(%ebp), %eax
imull -16(%ebp), %eax
movl %eax, %edx
movl -12(%ebp), %eax
imull -16(%ebp), %eax
leal (%edx,%eax), %ecx
movl -16(%ebp), %eax
mull -16(%ebp)
addl %edx, %ecx

這裡明顯可以看到正確的用在用32位進行64位乘法的步驟了。


你可以這樣寫

long a = 111111L * 111111L;

這樣是不會出錯的。

long a = 111111 * 111111;

111111是int型,這是2個int相乘,然後將結果轉為long。

第一種情況在編譯階段報錯,而第二種情況在編譯階段不報錯。

是因為對於第一種情況編譯器會做一些優化。

比如說long a = 3 * 3; 它在編譯階段就幫你算出來了,生成的指令和long a = 9是一樣的。


從編譯的角度看

你一個是int*int的結果「編譯器是如此認為的」

因此根據編譯器規則自然得到一個int

這個int超出了 所以gg

第二個 你用變數代替了數 或許編譯器沒做優化?所以編譯不報錯 然而同上 照樣溢出

第三個 由於b*b 都是long得到的結果也是long

所以不會溢出啦

第一次發現編譯原理的類型檢測還有點用orz

那麼為了預防溢出 在右邊加個long的強制轉換 應該就可以啦


首先概覽一下優先順序,賦值嘛,先算右值,右值就是111111*111111這個常量表達式,再賦給左值。我們只要看看這個過程中發生了什麼就好了。

______________分割線__________________

_______________________________________

先說第一個,long a = 111111 * 111111;,這個語句里 111111 * 111111 這部分是字面值,字面值的類型是系統默認分配的,111111,顯然系統默認這是一個int,這兩個int型字面值通過*連接,形成了一個表達式,表達式是有值的。值是有類型的,表達式中的操作數的類型就是表達式的類型,所以類型就是int。我們再看看這個表達式的值,這個表達式的值12345654321超過這個表達式值的類型的表達範圍了(莫名拗口( ????? )),因此就溢出了,這就是傳說中的overflow~

______________分割線__________________

_______________________________________

綜上所述,右值在被賦值給左值之前就溢出了,有符號型的溢出結果其實是未定義行為,不過這都是語言標準未定義,IDE一般都有明確一點的規則,顯然,在你的這幾個IDE里,值為負,看到了么?在賦值給左邊之前,已經是負的了。

你的第二種嘗試也是一個原理,不過到底怎麼報錯還是IDE的事,和語法無關,類似的錯誤當然可以有不同的報錯。

你的第三種嘗試是正確的,因為兩個操作數都是long,所以表達式值也就是long,因此就不溢出。(long足夠裝下12345654321這個數字)

相關知識點:運算符,操作數,左/右值,表達式,默認類型轉換,溢出,未定義行為。

嗯,其他大神也說的很清楚了,再深入的我也不懂就不說了。

歡迎訂閱並點贊。


數據類型的含義是一組數據的有限集合及其運算規則

你對數據類型的概念不夠清楚


推薦閱讀:

做 C 語言編譯器前端的難度如何?
開發效率與執行效率,我們應該怎樣斟酌?
為什麼大部分碼農做不了軟體架構師?
大學可以逃課自主學習嗎?

TAG:編程 | C編程語言 | 計算機科學 |