摸不透 C 語言

7 十月, 2008 (02:32) | 電腦與網路

我想,我還不夠 慣 C 吧… orz

今天蛙王問了我一個程式的問題,我想破頭不知道要如何解釋,想想還是貼出來,請教各方高人~

問題請見下面的程式,有註解的那行 (就是 C = 5 / 2; 下面那行) 打開和關閉會造成最下面的 printf() 印出不同的結果:

#include <stdio.h>

int main(void)
{
float C = 5 / 2;
// printf(" C = %f\n", C);
printf(" 5 / 2 = %f\n", 5 / 2 );
}

這就算了,最玄的還在這裡:把 C = 5 / 2; 改掉,例如說改成 C = 11 / 2; 好了,出來的結果也會不一樣;最後那個 printf() 多印幾次,結果也又不一樣喔…

我在 FreeBSD 6 / 7 上面試過 gcc 3.4.6 / 4.2.1 結果相同,不管 -O2 還是 -O0 也都一樣… 何解 ? 我是有叫它吐組語出來看看啦,不過我跟 AT&T 組語一向不是好朋友… Q_Q

PS. 我覺得最有可能的解釋是… 在 printf() 裡面,明明指定 %f 但是卻丟給它一個 int,這樣的行為是 unspecified 的…

Comments

Comment from biou
Time 2008/10/07 at 4:29 上午

學長好,

%f實際上指定的是"double" (通常是 8 bytes), 也就是說printf會去stack裡面,format string那個參數之後挖8 byte出來看。但只傳給他4個byte的int怎麼辦呢?那自然就會看到同一個stack frame裡面其他的東西(也許是其他的auto variable)所以就看到float C去了。

如果關掉那個float C的定義,那printf會看到stack更底下的東西,比如return address之類的?所以結果不一樣。

但我不太清楚為甚麼多來幾個printf也會有不同結果就是了。

Comment from 鳥毅
Time 2008/10/07 at 8:39 上午

回報一下,用msc結果是2.0,但是改成float C = 11 / 2;,結果就變成2048.0。(註解開否不影響)

Comment from Jazon
Time 2008/10/07 at 9:04 上午

挖printf的code出來看!o_o
結果大概會跟第一篇一樣

Comment from william
Time 2008/10/07 at 9:06 上午

「printf(” 5 / 2 = %f\n”, 5 / 2 );」這一行,「5 / 2」會被當成 int。

「float C = 5 / 2;」這一行,「5 / 2」最初也會被當成 int(所以數值為 2;而不是 2.5);等到要和左邊的「float C =」合併時,才會自 int 提升至 float。

Comment from william
Time 2008/10/07 at 9:14 上午

一樓 biou 說得沒錯:這要看當時 stack frame 內容是什麼。

多幾個 printf,擺在 stack frame 裡面的格式字串及 arguments 又增加許多,更增加結果的不確定性。

ijliao 直接回答他「這樣的行為是 unspecified 的」就好了… :)

Comment from Leo
Time 2008/10/07 at 1:55 下午

我用古老的DOS版 TC3.0 編譯完後執行沒有這種問題ㄟ, 所有Print出來的數值都一樣是 2.000000(如同版主所言已先被指定為int) 這會不會是compiler的問題啊~~

Comment from elleryq
Time 2008/10/08 at 1:09 上午

各家 compiler 的行為當然都不同…
不妨改為 printf(” 5 / 2 = %f\n”, 5.0f / 2 );
應該就沒問題啦~

Comment from biou
Time 2008/10/08 at 9:30 上午

elleryq:
應為printf(” 5 / 2 = %f\n”, (double) 5.0f / 2 );

Comment from 路人甲
Time 2008/10/08 at 4:23 下午

單純路過,幫忙討論一下
我覺得一樓應該是正解,只是我補充詳細點
平常我們自己寫的函式,呼叫函式一定要在丟參數時符合函式宣告上對於引數的規定
如果型態不對,那compiler會幫你做型態轉換再丟個warning出來,轉不過去就error
參數的數目不對就直接error了
可是printf系列的函式就直接打破這個規定
這是因為C支援「variable-length argument」函式,
關於這個原理可以參考 http://caterpillar.onlyfun.net/Gossip/CppGossip/VarArg.html
以下是個人不負責任猜測(因為我也不知道printf函式的實作內容)
我想compiler在看到函式的宣告後面是「…」之後,就不會去檢查那部份參數到底怎麼丟的了
我剛用Visual Studio 2005測試了一下
如果寫printf("%f %c %d\n", 10.0/2, ‘A’, 5/2, 33); (故意給多一個參數)
不但compile會過,而且也會產生exe,沒有error也沒有warning
而我猜printf的實作,其實是檢查字串的內容之後才去va_list分批把參數拿出來
所以compiler不負責檢查printf丟了什麼參數過去,linker更不可能在把printf程式碼貼過來時幫忙檢查
如果這時候printf的程式碼也不檢查參數內容符不符合,那就沒有人會檢查囉
總而言之,如果我們自己沒有在呼叫printf時把參數按照字串內的格式擺對(compiler會幫我們丟到va_list)
compiler不檢查,linker不檢查,printf的code自己也不檢查
那我們自己把va_list給搞爛掉應該就不是很意外的事……
所以在這邊怪compiler其實對它並不公平 XD
因為按照C的規定,compiler看到variable-length argument的「…」之後本來就不該去check參數
當然,也不會去幫你做型態轉換

所以板主的問題,我想只是戳到當初C語言設計時的一個漏洞而已
遇到variable-length argument的函式要更加小心使用

Comment from ijliao
Time 2008/10/08 at 11:47 下午

to 以上各位 : 您們講的其實我都知道,但是我想知道的卻沒人講 orz
"為什麼多呼叫幾次 printf() 會造成結果不相同 ?"

Comment from Yin
Time 2008/10/09 at 12:37 上午

我想是因為 call stack 被毀了,大一點的程式災情應該會很慘重

Comment from biou
Time 2008/10/09 at 1:45 上午

在MacOS 10.5.5上跟Debian/etch (on x86)上都無法複製出學長提到的現象:“為什麼多呼叫幾次 printf() 會造成結果不相同 ?”

相反地,在我這邊之後幾次的結果是相同的。(-O2 and -O0)

Comment from Yin
Time 2008/10/09 at 2:04 上午

修補一下… 想一想怪怪的 XDD

在 call 到最後一個 printf 裡的時候,我沒記錯的話 stack frame 應該大概是
[return addr] [指到 "5/2 = %f" 的指標] [int(2)] [!@#$] (printf 的 local variables)

因為那個 %f … 所以 printf 可能會把 "[!@#$] [int(2)]" 當成 float 印出來

而 [!@#$] 放的應該是第一個 printf(” C = %f\n”, C); 的 frame stack 的殘餘內容
可能是 C 的值,或是 local variable 的屍體

我無法重製第二個重複最後一行的 printf 得到不同解的狀況,也許跟 code size 改變有關 (比如那個 [!@#$] 剛好是 return address 之類的)

Comment from Yin
Time 2008/10/09 at 2:06 上午

BTW, 我是用 FreeBSD 6 + gcc 4.2 測試的

Comment from cibs
Time 2008/10/13 at 12:43 上午

我多呼叫幾次之後結果也還是一樣 @_@
FreeBSD 6.2 Release + gcc 3.4.6

Comment from AYE思博網
Time 2008/10/13 at 10:13 上午

我用古老的DOS版 TC3.0 編譯完後執行沒有這種問題ㄟ, 所有Print出來的數值都一樣是 2.000000(如同版主所言已先被指定為int) 這會不會是compiler的問題啊~~

Comment from Kyo
Time 2008/10/24 at 5:38 上午

ijliao, 把你那邊 "printf() 結果不相同" 產出的 AT&T 組語貼出來, 我幫你翻譯好了
因為我這邊再怎樣多呼叫幾次 printf() 得到的都是相同的結果

william, "float C = 11 / 2;" 這一行得要 "float C = (float) 11 / 2;"
才能得到並印出正確的結果, 它不會自動轉換或提升成正確的 float 數值
這不知道是不是 complier 的關係? :(

ps.印出那邊有無 (float) 都沒差, FreeBSD 6.2-RELEASE + gcc 3.4.6

Write a comment