【轉】函式的宣告和定義
一、函式的宣告
1.在C語言中,函式的定義順序是有講究的:預設情況下,只有後面定義的函式才可以呼叫前面定義過的函式
1 int sum(int a, int b) { 2 return a + b; 3 } 4 5 int main() 6 { 7 int c = sum(1, 4); 8 return 0; 9 }
第5行定義的main函式呼叫了第1行的sum函式,這是合法的。如果調換sum函式和main函式的順序,在標準的C編譯器環境下是不合法的(不過在GCC編譯器環境下只是一個警告)
2.如果想把函式的定義寫在main函式後面,而且main函式能正常呼叫這些函式,那就必須在main函式的前面進行函式的宣告
1 // 只是做個函式宣告,並不用實現 2 int sum(int a, int b); 3 4 int main() 5 { 6 int c = sum(1, 4); 7 return 0; 8 } 9 10 // 函式的定義(實現) 11 int sum(int a, int b) { 12 return a + b; 13 }
在第11行定義了sum函式,在第2行對sum函式進行了宣告,然後在第6行(main函式中)就可以正常呼叫sum函數了。
3.函式的宣告格式
1> 格式
返回值型別 函式名 (引數1, 引數2, ...)
只要你在main函式前面宣告過一個函式,main函式就知道這個函式的存在,就可以呼叫這個函式。而且只要知道函式名、函式的返回值、函式接收多少個引數、每個引數是什麼型別的,就能夠呼叫這個函數了,因此,宣告函式的時候可以省略引數名稱。比如上面的sum函式宣告可以寫成這樣:
int sum(int, int);
究竟這個函式是做什麼用的,還要看函式的定義。
2> 如果只有函式的宣告,而沒有函式的定義,那麼程式將會在連結時出錯
下面的寫法是錯誤的:
1 int sum(int a, int b); 2 3 int main() 4 { 5 6 sum(10, 11); 7 8 return 0; 9 }
- 在第1行聲明瞭一個sum函式,但是並沒有對sum函式進行定義,接著在第6行呼叫sum函式
- 這個程式是可以編譯成功的,因為我們在main函式前面聲明瞭sum函式(函式的宣告和定義是兩碼事),這個函式宣告可以理解為:在語法上,騙一下main函式,告訴它sum函式是存在的,所以從語法的角度上main函式是可以呼叫sum函式的。究竟這個sum函式存不存在呢,有沒有被定義呢?編譯器是不管的。在編譯階段,編譯器並不檢測函式有沒有定義,只有在連結的時候才會檢測這個函式存不存在,也就是檢測函式有沒有被定義。
- 因此,這個程式會在連結的時候報錯,錯誤資訊如下:
- 我這裡的原始檔是main.c檔案,所以編譯成功後生成一個main.o檔案。連結的時候,連結器會檢測main.o中的函式有沒有被定義。
- 上面的錯誤資訊大致意思是:在main.o檔案中找不到sum這個識別符號。
- 錯誤資訊中的linker是連結器的意思,下次看到這個linker,說明是連結階段出錯了。連結出錯了,就不能生成可執行檔案,程式就不能執行。
- 這個錯誤的解決方案就是加上sum函式的定義。
二、多原始檔開發
1.為什麼要有多個原始檔
1> 在編寫第一個c語言程式的時候已經提到:我們編寫的所有C語言程式碼都儲存在拓展名為.c的原始檔中,編寫完畢後就進行編譯、連結,最後執行程式。
2> 在前面的學習過程中,由於程式碼比較少,因此所有的程式碼都儲存在一個.c原始檔中。但是,在實際開發過程中,專案做大了,原始碼肯定非常多,很容易就上萬行程式碼了,甚至上十萬、百萬都有可能。這個時候如果把所有的程式碼都寫到一個.c原始檔中,那麼這個檔案將會非常龐大,也非常噁心,你可以想象一下,一個檔案有十幾萬行文字,不要說除錯程式了,連閱讀程式碼都非常困難。
3> 而且,公司裡面都是以團隊開發為主,如果多個開發人員同時修改一個原始檔,那就會帶來很多麻煩的問題,比如張三修改的程式碼很有可能會抹掉李四之前新增的程式碼。
4> 因此,為了模組化開發,一般會將不同的功能寫到不同的.c原始檔中,這樣的話,每個開發人員都負責修改不同的原始檔,達到分工合作的目的,能夠大大提高開發效率。也就是說,一個正常的C語言專案是由多個.c原始檔構成。
2.將sum函式寫到其他原始檔中
接下來就演示一下多個原始檔的開發,我將前面定義的sum函式寫在另一個原始檔(命名為sum.c)中。這時候就有兩個原始檔:
1> main.c檔案
1 int main() 2 { 3 4 return 0; 5 }
2> sum.c檔案
1 int sum(int a, int b) 2 { 3 return a + b; 4 }
3.在main函式中呼叫sum函式
1> 現在想在main函式中呼叫sum函式,那麼你可能會直接這樣寫:
1 int main() 2 { 3 int c = sum(10, 11); 4 5 return 0; 6 }
這種寫法在標準C語言編譯器中是直接報錯的,因為main函式都不知道sum函式的存在,怎麼可以呼叫它呢!!!
2> 我們應該騙一下main函式,sum函式是存在的,告訴它sum函式的返回值和引數型別即可。也就是說,應該在main函式前面,對sum函式進行宣告。
main.c檔案應該寫成下面這樣
1 #include <stdio.h> 2 3 int sum(int, int); 4 5 int main() 6 { 7 int c = sum(10, 11); 8 9 printf("c is %d\n", c); 10 11 return 0; 12 }
注意第3行,加了一個sum函式的宣告。為了檢驗sum函式的呼叫結果,在第9行用prinf函式將結果輸出。
4.編譯所有的原始檔
sum.c和main.c都編寫完畢後,就可以使用gcc指令進行編譯了。同時編譯兩個檔案的指令是:cc -c main.c sum.c
編譯成功後,生成了2個.o目標檔案
也可以單獨編譯:
cc -c main.c
cc -c sum.c
5.連結所有的目標檔案
前面已經編譯成功,生成了main.o和sum.o檔案。現在應該把這2個.o檔案進行連結,生成可執行檔案。
1> 注意,一定要同時連結兩個檔案。如果你只是單獨連結main.o或者sum.o都是不可能連結成功的。原因如下:
- 如果只是連結main.o檔案:cc main.o,錯誤資訊是:在main.o中找到不到sum這個識別符號,其實就是找不到sum函式的定義。因為sum函式的定義在sum.o檔案中,main.o中只有sum函式的宣告
- 如果只是連結sum.o檔案:cc sum.o,錯誤資訊是:找不到main函式。一個C程式的入口點就是main函式,main函式定義在main.o中,sum.o中並沒有定義main函式,連入口都沒有,怎麼能連結成功、生成可執行檔案呢?
可以看出,main.o和sum.o有密不可分的關係,其實連結的目的就是將所有相關聯的目標檔案和C語言函式庫組合在一起,生成可執行檔案。
2> 連結main.o和sum.o檔案:cc main.o sum.o,生成了可執行檔案a.out
3> 執行a.out檔案:./a.out,執行結果是在螢幕上輸出了:
c is 21
說明函式呼叫成功,我們已經成功在main.c檔案的main函式中呼叫了sum.c檔案中的sum函式
4> 從中也可以得出一個結論:只要知道某個函式的宣告,就可以呼叫這個函式,編譯就能成功。不過想要這個程式能夠執行成功,必須保證在連結的時候能找到函式的定義。
三、#include
理解完前面的知識後,接下來就可以搞懂一個很久以前的問題:每次寫在最前面的#include是幹啥用的?
1.#include的作用
先來看一個最簡單的C程式:
1 #include <stdio.h> 2 3 int main() 4 { 5 printf("Hello, World!\n"); 6 return 0; 7 }
這個程式的作用是在螢幕上輸出Hello,World!這一串內容,我們主要關注第一行程式碼。
- #include 是C語言的預處理指令之一,所謂預處理,就是在編譯之前做的處理,預處理指令一般以 # 開頭
- #include 指令後面會跟著一個檔名,前處理器發現 #include 指令後,就會根據檔名去查詢檔案,並把這個檔案的內容包含到當前檔案中。被包含檔案中的文字將替換原始檔中的 #include 指令,就像你把被包含檔案中的全部內容拷貝到這個 #include 指令所在的位置一樣。所以第一行指令的作用是將stdio.h檔案裡面的所有內容拷貝到第一行中。
- 如果被包含的檔案拓展名為.h,我們稱之為"標頭檔案"(Header File),標頭檔案可以用來宣告函式,要想使用這些函式,就必須先用 #include 指令包含函式所在的標頭檔案
- #include 指令不僅僅限於.h標頭檔案,可以包含任何編譯器能識別的C/C++程式碼檔案,包括.c、.hpp、.cpp等,甚至.txt、.abc等等都可以
也就是說你完全可以將第3行~第7行的程式碼放到其他檔案中,然後用 #include 指令包含進來,比如:
1> 將第3行~第7行的程式碼放到my.txt中
2> 在main.c原始檔中包含my.txt檔案
- 編譯連結後,程式還是可以照常執行的,因為 #include 的功能就是將檔案內容完全拷貝到 #include 指令所在的位置
- 說明:這裡用txt檔案純屬演示,平時做專案不會這樣做,除非吃飽了撐著,才會把程式碼都寫到txt中去
2.#include可以使用絕對路徑
上面的#include "my.txt"使用的是相對路徑,其實也可以使用絕對路徑。比如#include "/Users/apple/Desktop/my.txt"
3.#include <>和#include ""的區別
二者的區別在於:當被include的檔案路徑不是絕對路徑的時候,有不同的搜尋順序。
1> 對於使用雙引號""來include檔案,搜尋的時候按以下順序:
- 先在這條include指令的父檔案所在資料夾內搜尋,所謂的父檔案,就是這條include指令所在的檔案
- 如果上一步找不到,則在父檔案的父檔案所在資料夾內搜尋;
- 如果上一步找不到,則在編譯器設定的include路徑內搜尋;
- 如果上一步找不到,則在系統的INCLUDE環境變數內搜尋
2> 對於使用尖括號<>來include檔案,搜尋的時候按以下順序:
- 在編譯器設定的include路徑內搜尋;
- 如果上一步找不到,則在系統的INCLUDE環境變數內搜尋
我這裡使用的是clang編譯器,clang設定include路徑是(4.2是編譯器版本):/usr/lib/clang/4.2/include
Mac系統的include路徑有:
- /usr/include
- /usr/local/include
4.stdio.h
我們已經知道#include指令的作用了,可是為什麼要在第一行程式碼包含stdio.h呢?
- stdio.h 是C語言函式庫中的一個頭檔案,裡面聲明瞭一些常用的輸入輸出函式,比如往螢幕上輸出內容的printf函式
- 這裡之所以包含 stdio.h 檔案,是因為在第5行中用到了在 stdio.h 內部宣告的printf函式,這個函式可以向螢幕輸出資料,第7行程式碼輸出的內容是:Hello, World!
- 注意:stdio.h裡面只有printf函式的宣告。前面已經提到:只要知道函式的宣告,就可以呼叫這個函式,就能編譯成功。不過想要這個程式能夠執行成功,必須保證在連結的時候能找到函式的定義。其實連結除了會將所有的目標檔案組合在一起,還會關聯C語言的函式庫,函式庫中就有printf函式的定義。因此前面的程式是可以連結成功的。
5.標頭檔案.h和原始檔.c的分工
跟printf函式一樣,我們在開發中會經常將函式的宣告和定義寫在不同的檔案中,函式宣告放在.h標頭檔案中,函式定義放在.c原始檔中。
下面我們將sum函式的宣告和定義分別放在sum.h和sum.c中
這是sum.h檔案
這是sum.c檔案
然後在main.c中包含sum.h即可使用sum函式
其實sum.h和sum.c的檔名不一樣要相同,可以隨便寫,只要檔名是合法的。但還是建議寫成一樣,因為一看檔名就知道sum.h和sum.c是有聯絡的。
執行步驟分析:
1> 在編譯之前,預編譯器會將sum.h檔案中的內容拷貝到main.c中
2> 接著編譯main.c和sum.c兩個原始檔,生成目標檔案main.o和sum.o,這2個檔案是不能被單獨執行的,原因很簡單:
* sum.o中不存在main函式,肯定不可以被執行
* main.o中雖然有main函式,但是它在main函式中呼叫了一個sum函式,而sum函式的定義卻存在於sum.o中,因此main.o依賴於sum.o
3> 把main.o、sum.o連結在一起,生成可執行檔案
4> 執行程式
說到這裡,有人可能有疑惑:可不可以在main.c中包含sum.c檔案,不要sum.h檔案了?
大家都知道#include的功能是拷貝內容,因此上面的程式碼等效於:
這麼一看,語法上是絕對沒有問題的,main.c、sum.c都能編譯成功,分別生成sum.o、main.o檔案。但是當我們同時連結main.o和sum.o時會出錯。原因:當連結這兩個檔案時連結器會發現sum.o和main.o裡面都有sum函式的定義,於是報"識別符號重複"的錯誤,也就是說sum函式被重複定義了。預設情況下,C語言不允許兩個函式的名字相同。因此,不要嘗試去#include那些.c原始檔。
有人可能覺得分出sum.h和sum.c文件的這種做法好傻B,好端端多出2個檔案,你把所有的東西都寫到main.c不就可以了麼?
- 沒錯,整個C程式的程式碼是可以都寫在main.c中。但是,如果專案做得很大,你可以想象得到,main.c這個檔案會有多麼龐大,會嚴重降低開發和除錯效率。
- 要想出色地完成一個大專案,需要一個團隊的合作,不是一個人就可以搞的定的。如果把所有的程式碼都寫在main.c中,那就導致程式碼衝突,因為整個團隊的開發人員都在修改main.c檔案,張三修改的程式碼很有可能會抹掉李四之前新增的程式碼。
- 正常的模式應該是這樣:假設張三負責編寫 main函式,李四負責編寫其他自定義函式,張三需要用到李四編寫的某個函式,怎麼辦呢?李四可以將所有自定義函式的宣告寫在一個.h檔案中,比如 lisi.h,然後張三在他自己的程式碼中用#include包含lisi.h檔案,接著就可以呼叫lisi.h中宣告的函數了,而李四呢,可以獨立地在另外一個檔案中(比如lisi.c)編寫函式的定義,實現那些在lisi.h中宣告的函式。這樣子,張三和李四就可以相互協作、不會衝突。