首先,說這道題目“奇葩”並無任何不敬之意,相反它指出我知識的盲點,我是非常喜歡的。“奇葩”奇在你一般不該這么寫代碼。
今天參加一個知名外企的在線筆試,碰到一道另我小困惑了的C語言的題,題目如下:
#include <stdio.h> #include <stdlib.h> int i = 1; int main() { int static i; while(i<5) { printf("%d ",i); ++i; main(i); } return 0; }
請選擇該程序的輸出,選項如下:
A: 0 1 2 3 4 B: 1 2 3 4 5 C: runtime error D: compile error
分析:
題目的主要考點是:
1.能否認識到,main函數其實也是普通的函數,進行遞歸調用沒有問題。
2.變量作用域的問題,認識到這一點容易知道答案應該選擇A。(每次對i的引用都是對局部靜態變量的引用)
然而,令博主頗有些困惑的地方是: 對於函數定義參數列表為空的main函數,調用時卻對其賦了值,這是否會引起編譯時錯誤呢。平常的經驗告訴我,調用聲明參數列表為空的函數,向其傳入參數沒有問題,程序可以正常運行。往深問一層,這是為什么呢?(尾注1)
我進行了如下的測試(編譯環境gcc4.4.1, 編譯選項mingw32-gcc.exe -Wall -ansi -g -std=c99):
#include <stdio.h> #include <stdlib.h> int i = 1; int foo(); //int f(int i);//會導致后面多處編譯錯誤的聲明方法; int main() { int static i; while(i<5) { foo(); foo(i); foo(i,i); printf("%d\n",i); ++i; main(i); } return 0; } int foo(int i) { printf("%d\t",i); }
輸出為(經過freopen重定向至文件):
4227111 0 0 0 4227111 1 1 1 4227111 2 2 2 4227111 3 3 3 4227111 4 4 4
代碼中需要注意的有兩點:
1. foo函數的聲明與定義不一致,而這並沒有引起編譯錯誤(想一想,這是否與我們固有的認知不符?);
2. 對於聲明為參數列表為空的函數foo,無論使用多少參數它都可以工作;
依次分析這兩點:
1. C語言要求函數的聲明與定義一致,否則會有編譯錯誤,但是存在例外:
C99標准關於函數聲明一節(尾注2)中有如下一段話:
An identifier list declares only the identifiers of the parameters of the function. An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters. The empty list in a function declarator that is not part of a definition of that function specifies that no information about the number or types of the parameters is supplied.
意思是:
函數參數列表只會列出函數的參數。一個參數列表為空的函數聲明,若它是函數定義體的一部分,說明這是一個沒有參數的函數;
而若它不是函數定義體的一部分,則空的參數列表說明不提供任何函數參數數目與類型的信息。
對於上述代碼,由於foo函數的第一個聲明中參數列表為空,而此聲明不是函數定義的一部分,則說明此時沒有任何函數參數的信息,那么編譯器也就不會對之后的三次函數調用(函數符號的引用)進行參數檢查。於是,一段不符合常理的代碼編譯通過了。
2. 如前所述,此時編譯器不會對foo函數的調用進行參數檢查,而我們的執行結果也容易解釋了。函數foo被調用之前,向其傳遞的參數會被壓入棧中,foo函數執行時從棧指針固定位置取值作為i進行打印,也就出現了4227111(這是個動態的值)的情況。
由此可見,提供一個參數不詳的函數聲明,有導致危險行為的潛力(比如foo函數中有對字符串形參的打印)。雖然這某種程度會增加編程的靈活性,但是其危險性更大,可能這也能算是C語言一個缺陷級的設計。
然而,這里似乎出現了與我們看到現象不一致的問題。第一份代碼中的main函數的調用,main函數的唯一一個聲明正是它定義的一部分,根據C99標准,它是定義的一部分,它的參數列表為空,所以它指明函數是沒有參數的。然而,我們向main函數傳入任意個參數的時候,卻並未引起編譯錯誤。
再翻看標准,看到一個需要注意的地方:
The special case of an unnamed parameter of type void as the only item in the list specifies that the function has no parameters.
即:參數列表中只有一個沒有名字的void型的參數的這種特殊情況,指定函數是沒有參數的。
所以,只能暫下結論,在gcc 4.4.1的環境里:
1.對於參數列表為空的函數聲明,即使它是函數定義的一部分,對他進行調用時傳入參數是不會導致編譯時錯誤的,雖然這與標准並不一致(尾注3);
2.看到參數列表為空的函數聲明時,若它不是函數定義的一部分,請敏感起來;
3.培養起良好的習慣,把空的參數列表寫成void吧。
====================================尾注==========================================
尾注1:最近參加不少外企的筆試,碰到一些綜合性的C語言的題目,感慨畢竟是有底蘊的大公司啊,各種在C上血虐我。(比如:oracle)。
尾注2:6.7.5.3 Function declarators(including prototypes) 原諒博主的懶惰,忙於找工作,暫時沒時間看C11了。
尾注3:敬請期待下集,函數調用過程中的參數壓棧,究竟依據何處,函數聲明、函數定義,還是函數調用?