現代編譯器是如何實現自定義函數在main()函數之後實現的呢?

C語言老師說嚴格來說自定義函數得在main()函數前實現,或者在前面聲明,但在很多IDE上是可以這樣用的,這個是怎麼實現的呢?


題主是想說這樣的情況么:

一個完整的xyz.c文件:

int main() {
int d = foo(3, 4);
printf("%d
", d);
return 0;
}

int foo(int x, int y) {
return x - y;
}

(是的,沒有前置聲明,沒有#include)

如果用Clang以默認參數編譯的話會得到2個警告:

$ clang -g xyz.c
xyz.c:2:11: warning: implicit declaration of function "foo" is invalid in C99
[-Wimplicit-function-declaration]
int d = foo(3, 4);
^
xyz.c:3:3: warning: implicitly declaring library function "printf" with type
"int (const char *, ...)"
printf("%d
", d);
^
xyz.c:3:3: note: please include the header & or explicitly provide a declaration for
"printf"
2 warnings generated.

同學們要留意了:C語言是一門古老的語言,所以它有許多設計(特別是早期設計)帶著濃厚的歷史色彩。這裡要說的「功能」就顯示出了C語言設計的時候對單趟編譯器(one-pass compiler)的傾向。

C89語言規範有implicit function declaration的規定:如果在程序里看到了對未聲明的函數的調用,例如說foo(1, 2, 3),則會隱式聲明一個int foo()。注意這裡參數列表為空並不代表 foo() 不接受任何參數,而是代表它可以接受任意多個參數;要表達不接受任何參數要聲明為 int foo(void) 。

Declarations - cppreference.com

In C89, declarations within any compound statement (block scope) must appear in the beginning of the block, before any statements.

Also, in C89, functions returning int may be implicitly declared by the function call operator and function parameters of type int do not have to be declared when using old-style function definitions.

另外請參考KR書里對這個老功能的描述:《The C Programming Language》的筆記-第72頁

C99規範開始這個implicit function declaration的功能就被禁用了。

GCC和Clang的話指定"-pedantic-errors"參數會把上述兩個警告視為錯誤,符合C99開始的語言規定。

當然這個implicit function declaration的功能是有危險的,不然幹嘛取消掉呢。

想想,編譯器要怎麼處理一個能接受任意個數參數的函數聲明呢?最簡單的辦法就是假定它的calling convention是一個「標準的」calling convention,並且使用能接受varargs版的calling convention。

以32位Windows x86版上MSVC的默認calling convention,cdecl為例:參數全部通過調用棧來傳遞,從右向左壓棧,整型和指針返回值通過EAX返回。

但是如果我們的被調用函數實際上是一個指定使用__fastcall、返回float的函數呢?它的頭2個整型或指針參數從ECX、EDX傳遞,剩餘參數從棧上傳遞,從右向左壓棧;浮點返回值通過x87 ST(0)返回。

這結果就是類似於:

/* implicitly declared foo:
int __cdecl foo();
*/

int bar() {
return foo(2, 3); /* 假定foo是cdecl, 通過棧傳遞參數, 通過EAX取返回值 */
}

float __fastcall foo(int x, int y) {
/* x在ECX, y在EDX, 返回值在x87 ST(0)*/
return (float) x - y;
}

這就完全錯位了對不對?

且不提參數個數不匹配啊之類的別的錯誤情況。


1. 這和現代不現代無關。C標準的都行。

2. 「但」這個字是怎麼回事,你的前後沒有轉折啊。

3. 這和IDE無關,和編譯器有關。

實現上,就是編譯到一個函數調用的時候,只是看聲明。聲明能對上就行。之後再來一遍把真正的函數實現地址放過去。


正常情況下兩遍編譯就行了吧,一遍編譯不能在後面的原因是還沒看到那個函數,不知道怎麼處理。

畢竟彙編都可以往後跳,彙編器在遇到不認識的標號的時候會在這個call或者jcc的地方填nop,等找到那個標號之後再回過頭填上坑,當然這裡有可能會多填nop進去(因為要算地址所以不能動)……

所以其實理論上說,C本來就應該連聲明都不需要的,甚至extern也不用,完全可以學習一下java和csharp的方法嘛


首先要問,你有沒有限制條件,比如必須是一趟編譯?

如果沒有,那麼我可以第一趟把所有的函數聲明提出來,然後就和正常編譯一樣了。

如果必須一趟,那麼我可以把尚未定義的函數調用整理到一張表,然後在編譯後面的函數是從這個表裡刪除這個函數。

方法可以有很多,這個我在做自己的c語言編譯器的時候考慮過。

比較老的編譯器不支持這種寫法,必須先聲明,再使用。因為這樣編譯起來更舒服。

我建議你寫一個小工程的時候,最好把所有函數聲明在一個頭文件裡面,然後每個.c文件都include一下。如果你想詳細了解如何從文件上組織一個工程,推薦一個網站給你http://www.baidu.com


請學電腦之前分清編譯和連接!!!

70年代的C語言語法與現在不同,因此暫不討論。只考慮c89之後的。

編譯器會猜一個返回int的聲明,它才不管對不對,也沒法管,不然就得讓C語言不允許函數重名了。然後連接器可能會蒙,這樣連接器就有可能連接到一個錯誤的函數上,畢竟C語言里不同類型的函數是可以重名的。

如果你自己永遠保證編譯器猜的類型和你想寫的一樣,那當然可以不在前面聲明。就像你可以手動查表翻譯彙編程序為機器碼而不用彙編器一樣,替機器做重複勞動而已,是非常low的行為。

至於為什麼不讓編譯器編譯多份供連接器選擇,因為這樣對嵌套的函數調用和多參是指數複雜度的。

至於為什麼不讓編譯器問連接器可能存在哪些聲明,因為連接器以及其他幾個同名函數的實現可能不在這台編譯用的計算機上。

在實際的項目里,我們幾乎不會在.c文件里寫聲明,因為完全沒有必要,寫在.h文件里include進來就好了啊。


從彙編時代就有這東西。比如你要跳到某個 LABEL,然後它在下面,彙編器讀到這行的時候必然不知道它是啥。那就先空著,掃描兩遍唄,第二遍的時候符號表完善了,就知道了。


在main函數前先聲明一下你調用的函數就能調用了


編譯器預編譯階段,符號解析就可以實現了。沒啥難度。題主的問題估計只和C標準定義有關吧


推薦閱讀:

C++ 「==」運算符取值問題?
每次編程開始先輸入 #include,那麼計算機在讀取 # 符號的時候 正在做什麼?
C++ 和 Objective-C 都可以 100% 翻譯為 C 代碼嗎?
搞oi/acm的大神為什麼要#define N 1000+10?
運行時異常處理程序是如何實現的?

TAG:編程 | 集成開發環境 | CC | 編譯器 |