func(void):C 與 C++ 函式宣告的小差異
最近編譯遇到一個錯誤訊息 warning: function declaration isn’t a prototype,程式碼大概是像這樣:
hello.h
1
2
3
4
| void foo()
{
printf("Hello World!\n");
} |
1
2
3
4
5
| int main(int argc, char**argv)
{
foo();
return0;
} |
函式的宣告(Declaration)、原型(Prototype)、與定義(Definition)
在公布解答前,先來解釋一下這三個名詞。
如果只是為了讓其他檔案知道有這個函式的存在,我們使用 Declaration 來代表告知的動作。雖然 C 語言並沒有支援 Function Overloading,我們不需要擔心可能會有同名的函式,但是卻有可能因為呼叫端傳入的參數不如預期,導致錯誤延遲到執行期間才被發現,會讓我們除錯的成本大幅度地增加。因此,為了在編譯階段就可以找出這些錯誤,ANSI C 導入了 Prototype 的觀念,除了與 Declaration 一樣具有告知的功能,還必須明確地表示出傳入的參數型態。
=> 也就是說 Prototype 就是 Declaration,但是 Declaration 未必是 Prototype。
搞懂前兩個後,最後的 Definition 就簡單了,它就是函式的實作。舉例來說:
1
2
3
4
5
6
| int foo();// Declaration
int foo(int, int);// Prototype
int foo(int a, int b)// Definition
{
printf("a+b = %d\n", a+b);
} |
在 C89 出現之前(1983~),我們習慣稱我們所使用的 C 語言格式為 Pre-ANSI C (或稱 K&R C)。之所以特別強調是 old-style 的原因是在 ANSI C 的標準中規定 Declaration 也要包含 Prototype(C89 後,宣告與原型其實是同樣的東西,這也是容易讓我們年輕人搞混的原因XD),但為了相容老舊的程式碼,這種 old-style declaration 直到 C99 還是被允許的。
老舊的宣告格式所帶來的問題
在前面有稍微提到,之所以引入 Prototype 就是為了能在編譯階段提前找出錯誤。我們以下列程式碼範例來看看實際上可能會遇到的錯誤(範例碼參考自此):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| int imax();
int main(void)
{
printf("%lu %lu %lu\n", sizeof(int), sizeof(double), sizeof(float));
printf("The maximum of %d and %d is %d.\n", 3, 5, imax(5));
printf("The maximum of %d and %d is %d.\n", 3, 5, imax(3.0, 5.0));
return0;
}
int imax(n, m)
int n, m;
{
int max;
if(n > m)
max = n;
else
max = m;
return max;
} |
以我執行環境來看,int/double/float 的大小分別為 4/8/4 bytes,因為 old-style 的宣告方式不會在編譯階段檢查傳入參數的正確性,因此在我們看來顯而易見的錯誤(程式行數少)就這樣被埋下了。
- 第一個呼叫中,我們只傳入一個 4-byte int,函式執行時,卻去 stack 中 pop 兩個 4-byte int,而通常記憶體內容值要小於 5 的機率實在太小的,所以你會看到每次執行結果都不同,但是怎麼樣也不會是 5。
- 第二個呼叫中,我們傳入了兩個浮點數,必須注意的是浮點數若沒有特別轉型,一律會被自動被視為 double 型態,也就是說我們傳入了兩個 8-byte double 而非兩個 4-byte float。
解決方法
經過以上的說明後,再回過頭看回到最先的問題 warning: function declaration isn’t a prototype 我想大家就會覺得這句很白話了,意思就是:「警告:函式宣告沒有明確寫出傳入的參數型態」。
常寫 C++ 的人可能會好奇問說:「我也沒加 void 也沒跳出警告啊?」。C++ 在這一部分的確沒有如 C 語言一般的包袱,也就是說,除非使用
在 ANSI C 中,建議大家還是以 Prototype 來宣告函式,**如果真的沒有要傳入任何參數,也請加上 void **,這樣才可以讓編譯器在編譯階段自動檢查。
K&R, pages 72-73:
Furthermore, if a function declaration does not include arguments, as in
double atof(); that too is taken to mean that nothing is to be assumed about the arguments of atof; all parameter checking is turned off. This special meaning of the empty argument list is intended to permit older C programs to compile with new compilers. But it’s a bad idea to use it with new programs. If the function takes arguments, declare them; if it takes no arguments, use void.