譯自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.out
42
$ echo $?
3
$ cc–std=c99 –c foo.c
$ ccfoo.o
$ ./a.out
42
$ 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,那麼這個變量就變成該編譯單元的局部變量了,其他編譯單元不可以通過鏈接器訪問到該變量。
你:不錯。接下來,將展示一些很不錯的玩意。靜候:)
摘自 Rockics的專欄