譯自Deep C (and C++) by Olve Maudal and Jon Jagger,本身半桶水不到,如果哪位網友發現有錯,留言指出吧:)
好,接著深入理解C/C++之旅。我在翻譯第一篇的時候,自己是學到不不少東西,因此打算將這整個ppt翻譯完畢。
請看下面的代碼片段:
#include <stdio.h>
void foo(void)
{
int a;
printf("%d\n", a);
}
void bar(void)
{
int a = 42;
}
int main(void)
{
bar();
foo();
}
編譯運行,期待輸出什麼呢?
$ cc foo.c && ./a.out
42
你可以解釋一下,為什麼這樣嗎?
第一個候選者:嗯?也許編譯器為了重用有一個變量名稱池。比如說,在bar函數中,使用並且釋放了變量a,當foo函數需要一個整型變量a的時候,它將得到和bar函數中的a的同一內存區域。如果你在bar函數中重新命名變量a,我不覺得你會得到42的輸出。
你:恩。確定。。。
第二個候選者:不錯,我喜歡。你是不是希望我解釋一下關於執行堆棧或是活動幀(activation frames, 操作代碼在內存中的存放形式,譬如在某些系統上,一個函數在內存中以這種形式存在:
ESP
形式參數
局部變量
EIP
)?
你:我想你已經證明了你理解這個問題的關鍵所在。但是,如果我們編譯的時候,采用優化參數,或是使用別的編譯器來編譯,你覺得會發生什麼?
候選者:如果編譯優化措施參與進來,很多事情可能會發生,比如說,bar函數可能會被忽略,因為它沒有產生任何作用。同時,如果foo函數會被inline,這樣就沒有函數調用了,那我也不感到奇怪。但是由於foo函數必須對編譯器可見,所以foo函數的目標文件會被創建,以便其他的目標文件鏈接階段需要鏈接foo函數。總之,如果我使用編譯優化的話,應該會得到其他不同的值。
$ cc -O foo.c && ./a.out
1606415608
候選者:垃圾值。
那麼,請問,這段代碼會輸出什麼?
#include <stdio.h>
void foo(void)
{
int a = 41;
a= a++;
printf("%d\n", a);
}
int main(void)
{
foo();
}
第一個候選者:我沒這樣寫過代碼。
你:不錯,好習慣。
候選者:但是我猜測答案是42.
你:為什麼?
候選者:因為沒有別的可能了。
你:確實,在我的機器上運行,確實得到了42.
候選者:對吧,嘿嘿。
你:但是這段代碼,事實上屬於未定義。
候選者:對,我告訴過你,我沒這樣寫過代碼。
第二個候選者登場:a會得到一個未定義的值。
你:我沒有得到任何的警告信息,並且我得到了42.
候選者:那麼你需要提高你的警告級別。在經過賦值和自增以後,a的值確實未定義,因為你違反了C/C++語言的根本原則中的一條,這條規則主要針對執行順序(sequencing)的。C/C++規定,在一個序列操作中,對每一個變量,你僅僅可以更新一次。這裡,a = a++;更新了兩次,這樣操作會導致a是一個未定義的值。
你:你的意思是,我會得到一個任意值?但是我確實得到了42.
候選者:確實,a可以是42,41,43,0,1099,或是任意值。你的機器得到42,我一點都不感到奇怪,這裡還可以得到什麼?或是編譯前選擇42作為一個未定義的值:)呵呵:)
那麼,下面這段代碼呢?
#include <stdio.h>
int b(void)
{
puts("3");
return 3;
}
int c(void)
{
puts("4");
return 4;
}
int main(void)
{
int a = b() + c();
printf("%d\n", a);
}
第一個候選者:簡單,會依次打印3,4,7.
你:確實。但是也有可能是4,3,7.
候選者:啊?運算次序也是未定義?
你:准確的說,這不是未定義,而是未指定。
候選者:不管怎樣,討厭的編譯器。我覺得他應該給我們警告信息。
你心裡默念:警告什麼?
第二個候選者:在C/C++中,運算次序是未指定的,對於具體的平台,由於優化的需要,編譯器可以決定運算順序,這又和執行順序有關。
這段代碼是符合C標准的。這段代碼或是輸出3,4,7或是輸出4,3,7,這個取決於編譯器。
你心裡默念:要是我的大部分同事都像你這樣理解他們所使用的語言,生活會多麼美好:)
這個時候,我們會覺得第二個候選者對於C語言的理解,明顯深刻於第一個候選者。如果你回答以上問題,你停留在什麼階段?:)
那麼,試著看看第二個候選者的潛能?看看他到底有多了解C/C++
可以考察一下相關的知識:
聲明和定義;
調用約定和活動幀;
序點;
內存模型;
優化;
不同C標准之間的區別;
這裡,我們先分享序點以及不同C標准之間的區別相關的知識。
考慮以下這段代碼,將會得到什麼輸出?
1.
int a = 41;
a++;
printf("%d\n", a);
答案:42
2.
int a = 41;
a++ & printf("%d\n", a);
答案:未定義
3.
int a = 41;
a++ && printf("%d\n", a);
答案:42
4. int a = 41;
if (a++ < 42) printf("%d\n",a);
答案:42
5.
int a = 41;
a = a++;
printf("%d\n", a);
答案:未定義
到底什麼時候,C/C++語言會有副作用?
序點:
什麼是序點?
簡而言之,序點就是這麼一個位置,在它之前所有的副作用已經發生,在它之後的所有副作用仍未開始,而兩個序點之間所有的表達式或者代碼執行的順序是未定義的!
序點規則1:
在前一個序點和後一個序點之前,也就是兩個序點之間,一個值最多只能被寫一次;
這裡,在兩個序點之間,a被寫了兩次,因此,這種行為屬於未定義。
序點規則2:
進一步說,先前的值應該是只讀的,以便決定要存儲什麼值。
很多開發者會覺得C語言有很多序點,事實上,C語言的序點非常少。這會給編譯器更大的優化空間。
接下來看看,各種C標准之間的差別:
現在讓我們回到開始那兩位候選者。
下面這段代碼,會輸出什麼?
#include <stdio.h>
struct X
{
int a;
char b;
int c;
};
int main(void)
{
printf("%d\n", sizeof(int));
printf("%d\n", sizeof(char));
printf("%d\n", sizeof(struct X));
}
第一個候選者:它將打印出4,1,12.
你:確實,在我的機器上得到了這個結果。
候選者:當然。因為sizeof返回字節數,在32位機器上,C語言的int類型是32位,或是4個字節。char類型是一個字節長度。在struct中,本例會以4字節來對齊。
你:好。
你心裡默念:do you want another ice cream?(不知道有什麼特別情緒)
第二個候選者:恩。首先,先完善一下代碼。sizeof的返回值類型是site_t,並不總是與int類型一樣。因此,printf中的輸出格式%d,不是一個很好的說明符。
你:好。那麼,應該使用什麼格式說明符?
候選者:這有點復雜。site_t是一個無符號整型數,在32位機器上,它通常是一個無符號的int類型的數,但是在64位機器上,它通常是一個無符號的long類型的數。然而,在C99中,針對site_t類型,指定了一個新的說明符,所以,%zu會是一個不多的選擇。
你:好。那我們先完善這個說明符的bug。你接著回答這個問題吧。
#include <stdio.h>
struct X
{
int a;
char b;
int c;
};
int main(void)
{
printf("%zu\n", sizeof(int));
printf("%zu\n", sizeof(char));
printf("%zu\n", sizeof(struct X));
}
候選者:這取決與平台,以及編譯時的選項。唯一可以確定的是,sizeof(char)是1.你要假設在64位機器上運行嗎?
你:是的。我有一台64位的機器,運行在32位兼容模式下。
候選者:那麼由於字節對齊的原因,我覺得答案應該是4,1,12.當然,這也取決於你的編譯選項參數,它可能是4,1,9.如果你在使用gcc編譯的時候,加上-fpack-struct,來明確要求編譯器壓縮struct的話。
你:在我的機器上確實得到了4,1,12。為什麼是12呢?
候選者:工作在字節不對齊的情況下,代價非常昂貴。因此編譯器會優化數據的存放,使得每一個數據域都以字邊界開始存放。struct的存放也會考慮字節對齊的情況。
你:為什麼工作在字節不對齊的情況下,代價會很昂貴?
候選者:大多數處理器的指令集都在從內存到cpu拷貝一個字長的數據方面做了優化。如果你需要改變一個橫跨字邊界的值,你需要讀取兩個字,屏蔽掉其他值,然後改變再寫回。可能慢了10不止。記住,C語言很注意運行速度。
你:如果我得struct上加一個char d,會怎麼樣?
候選者:如果你把char d加在struct的後面,我預計sizeof(struct X)會是16.因為,如果你得到一個長度為13字節的結構體,貌似不是一個很有效的長度。但是,如果你把char d加在char b的後面,那麼12會是一個更為合理的答案。
你:為什麼編譯器不重排結構體中的數據順序,以便更好的優化內存使用和運行速度?
候選者:確實有一些語言這樣做了,但是C/C++沒有這樣做。
你:如果我在結構體的後面加上char *d,會怎麼樣?
候選者:你剛才說你的運行時環境是64位,因此一個指針的長度的8個字節。也許struct的長度是20?但是另一種可能是,64位的指針需要在在效率上對齊,因此,代碼可能會輸出4,1,24?
你:不錯。我不關心在我的機器上會得到什麼結果,但是我喜歡你的觀點以及洞察力J
(未完待續)
靜候
摘自 Rockics的專欄