譯自Deep C (and C++) by Olve Maudal and Jon Jagger,本身半桶水不到,如果哪位網友發現有錯,留言指出吧:)
編程是困難的,正確的使用C/C++編程尤其困難。確實,不管是C還是C++,很難看到那種良好定義並且編寫規範的代碼。為什麼專業的程式員寫出這樣的代碼。因為絕大部分程式員都沒有深刻的理解他們所使用的語言。他們對語言的把握,有時他們知道某些東西未定義或未指定,但經常不知道為何如此。這個投影片,我們將研究一些小的C/C++程式碼片段,使用這些程式碼片段,我們將討論這些偉大而充滿危險的語言的基本原則,局限性,以及設計哲學。
假設你將要為你的公司招聘一名C程式言,你們公司是做嵌入式開發的,為此你要面試一些候選人。作為面試的一部分,你希望通過面試知道候選人對於C語言是否有足夠深入的認識,你可以這樣開始你們的談話:
int main (){ int a= 42; printf(“%d\n”,a);}
當你嘗試去編譯連結運行這段代碼時候,會發生什麼。
一個候選者可能會這樣回答:
你必須通過#include<stdio.h>包含標頭檔,在程式的後面加上 return 0; 然後編譯連結,運行以後將在螢幕上列印42.
沒錯,這個答案非常正確。
但是另一個候選者也許會抓住機會,藉此展示他對C語言有更深入的認識,他會這樣回答:
你可能需要#include<stdio.h>,這個標頭檔顯示地定義了函數printf(),這個程式經過編譯連結運行,會在標準輸出上輸出42,並且緊接著新的一行。
然後他進一步說明:
C++編譯器將會拒絕這段代碼,因為C++要求必須顯示定義所有的函數。然而,有一些特別的C編譯器會為printf()函數建立隱式定義,把這個檔案編譯成目標檔案。再跟標準庫連結的時候,它將尋找printf()函數的定義,以此來匹配隱式的定義。
因此,上面這段代碼也會正常編譯、連結然後運行,當然你可能會得到一些警告資訊。
這位候選者乘勝追擊,可能還會往下說,如果是C99,傳回值被定義為給運行環境指示是否運行成功,正如C++98一樣。但是對於老版本的C語言,比如說ANSI C以及K&R C,程式中的傳回值將會是一些未定義的垃圾值。但是傳回值通常會使用寄存器來傳遞,如果傳回值的3,我一點都不感到驚訝,因為printf()函數的傳回值是3,也就是輸出到標準輸出的字元個數。
說到C標準,如果你要表明你關心C語言,你應該使用 intmain (void)作為你的程式入口,因為標準就這麼說的。
C語言中,使用void來指示函式宣告中不需要參數。如果這樣聲明函數int f(),那表明f()函數可以有任意多的參數,雖然你可能打算說明函數不需要參數,但這裡並非你意。如果你的意思是函數不需要參數,顯式的使用void,並沒有什麼壞處。
int main (void){ inta = 42; printf(“%d\n”,a);}
然後,有點炫耀的意思,這位候選人接著往下說:
如果你允許我有點點書生氣,那麼,這個程式也並不完全的符合C標準,因為C標準指出原始碼必須要以新的一行結束。像這樣:
int main (){ inta = 42; printf(“%d\n”,a);}
同時別忘了顯式的聲明函數printf():
#include <stdio.h>int main (void){ inta = 42; printf(“%d\n”,a);}
現在看起來有點像C程式了,對嗎。
然後,在我的機器上編譯、連結並運行此程式:
$ cc–std=c89 –c foo.c$ ccfoo.o$ ./a.out42$ echo $?3 $ cc–std=c99 –c foo.c$ ccfoo.o$ ./a.out42$ echo $?0
這兩名候選者有什麼區別嗎。是的,沒有什麼特別大的區別,但是你明顯對第二個候選者的答案更滿意。
也許這並不是真的候選者,或許就是你的員工,呵呵。
讓你的員工深入理解他們所使用的語言,對你的公司會有很大協助嗎。
讓我們看看他們對於C/C++理解的有多深……
#include <stdio.h> void foo(void){ int a = 3; ++a; printf("%d\n", a);} int main(void){ foo(); foo(); foo();}
這兩位候選者都會是,輸出三個4.然後看這段程式:
#include <stdio.h> void foo(void){ static int a = 3; ++a; printf("%d\n", a);} int main(void){ foo(); foo(); foo();}
他們會說出,輸出4,5,6.再看:
#include <stdio.h> void foo(void){ static int a; ++a; printf("%d\n", a);} int main(void){ foo(); foo(); foo();}
第一個候選者發出疑問,a未定義,你會得到一些垃圾值。
你說:不,會輸出1,2,3.
候選者:為什麼。
你:因為靜態變數會被初始化未0.
第二個候選者會這樣來回答:
C標準說明,靜態變數會被初始化為0,所以會輸出1,2,3.
再看下面的程式碼片段:
#include <stdio.h> void foo(void){ int a; ++a; printf("%d\n", a);} int main(void){ foo(); foo(); foo();}
第一個候選者:你會得到1,1,1.
你:為什麼你會這樣想。
候選者:因為你說他會初始化為0.
你:但這不是靜態變數。
候選者:哦,那你會得到垃圾值。
第二個候選者登場了,他會這樣回答:
a的值沒有定義,理論上你會得到三個垃圾值。但是實踐中,因為自動變數一般都會在運行棧中分配,三次調用foo函數的時候,a有可能存在同一記憶體空間,因此你會得到三個連續的值,如果你沒有進行任何編譯最佳化的話。
你:在我的機器上,我確實得到了1,2,3.
候選者:這一點都不奇怪。如果你運行於debug模式,運行時機制會把你的棧空間全部初始化為0.
接下來的問題,為什麼靜態變數會被初始化為0,而自動變數卻不會被初始化。
第一個候選者顯然沒有考慮過這個問題。
第二個候選者這樣回答:
把自動變數初始化為0的代價,將會增加函數調用的代價。C語言非常注重運行速度。
然而,把全域變數區初始化為0,僅僅在程式啟動時候產產生本。這也許是這個問題的主要原因。
更精確的說,C++並不把靜態變數初始化為0,他們有自己的預設值,對於原生類型(native types)來說,這意味著0。
再來看一段代碼:
#include<stdio.h> static int a; void foo(void){ ++a; printf("%d\n", a);} int main(void){ foo(); foo(); foo();}
第一個候選者:輸出1,2,3.
你:好,為什麼。
候選者:因為a是靜態變數,會被初始化為0.
你:我同意……
候選者:cool…
這段代碼呢:
#include<stdio.h> int a; void foo(void){ ++a; printf("%d\n", a);} int main(void){ foo(); foo(); foo();}
第一個候選者:垃圾,垃圾,垃圾。
你:你為什麼這麼想。
候選者:難道它還會被初始化為0。
你:是的。
候選者:那他可能輸出1,2,3。
你:是的。你知道這段代碼跟前面那段代碼的區別嗎。 有static那一段。
候選者:不太確定。等等,他們的區別在於私人變數(private variables)和公有變數(public variables).
你:恩,差不多。
第二個候選者:它將列印1,2,3.變數還是靜態分配,並且被初始化為0.和前面的區別:嗯。這和連結器(linker)有關。這裡的變數可以被其他的編譯單元訪問,也就是說,連結器可以讓其他的目標檔案訪問這個變數。但是如果加了static,那麼這個變數就變成該編譯單元的局部變數了,其他編譯單元不可以通過連結器訪問到該變數。
你:不錯。接下來,將展示一些很不錯的玩意。靜候:)