天天看點

【C語言】11-函數的聲明和定義

一、函數的聲明

1.在C語言中,函數的定義順序是有講究的:預設情況下,隻有後面定義的函數才可以調用前面定義過的函數

【C語言】11-函數的聲明和定義
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 }      
【C語言】11-函數的聲明和定義

第5行定義的main函數調用了第1行的sum函數,這是合法的。如果調換sum函數和main函數的順序,在标準的C編譯器環境下是不合法的(不過在GCC編譯器環境下隻是一個警告)

2.如果想把函數的定義寫在main函數後面,而且main函數能正常調用這些函數,那就必須在main函數的前面進行函數的聲明

【C語言】11-函數的聲明和定義
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 }      
【C語言】11-函數的聲明和定義

在第11行定義了sum函數,在第2行對sum函數進行了聲明,然後在第6行(main函數中)就可以正常調用sum函數了。

3.函數的聲明格式

1> 格式

傳回值類型  函數名 (參數1, 參數2, ...)      

隻要你在main函數前面聲明過一個函數,main函數就知道這個函數的存在,就可以調用這個函數。而且隻要知道函數名、函數的傳回值、函數接收多少個參數、每個參數是什麼類型的,就能夠調用這個函數了,是以,聲明函數的時候可以省略參數名稱。比如上面的sum函數聲明可以寫成這樣:

int sum(int, int);      

究竟這個函數是做什麼用的,還要看函數的定義。

2> 如果隻有函數的聲明,而沒有函數的定義,那麼程式将會在連結時出錯

下面的寫法是錯誤的:

【C語言】11-函數的聲明和定義
1 int sum(int a, int b);
2 
3 int main()
4 {
5     
6     sum(10, 11);
7 
8     return 0;
9 }      
【C語言】11-函數的聲明和定義
  • 在第1行聲明了一個sum函數,但是并沒有對sum函數進行定義,接着在第6行調用sum函數
  • 這個程式是可以編譯成功的,因為我們在main函數前面聲明了sum函數(函數的聲明和定義是兩碼事),這個函數聲明可以了解為:在文法上,騙一下main函數,告訴它sum函數是存在的,是以從文法的角度上main函數是可以調用sum函數的。究竟這個sum函數存不存在呢,有沒有被定義呢?編譯器是不管的。在編譯階段,編譯器并不檢測函數有沒有定義,隻有在連結的時候才會檢測這個函數存不存在,也就是檢測函數有沒有被定義。
  • 是以,這個程式會在連結的時候報錯,錯誤資訊如下:
【C語言】11-函數的聲明和定義
  • 我這裡的源檔案是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函數,那麼你可能會直接這樣寫:

【C語言】11-函數的聲明和定義
1 int main()
2 {
3     int c = sum(10, 11);
4 
5     return 0;
6 }      
【C語言】11-函數的聲明和定義

這種寫法在标準C語言編譯器中是直接報錯的,因為main函數都不知道sum函數的存在,怎麼可以調用它呢!!!

2> 我們應該騙一下main函數,sum函數是存在的,告訴它sum函數的傳回值和參數類型即可。也就是說,應該在main函數前面,對sum函數進行聲明。

main.c檔案應該寫成下面這樣

【C語言】11-函數的聲明和定義
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 }      
【C語言】11-函數的聲明和定義

注意第3行,加了一個sum函數的聲明。為了檢驗sum函數的調用結果,在第9行用prinf函數将結果輸出。

4.編譯所有的源檔案

sum.c和main.c都編寫完畢後,就可以使用gcc指令進行編譯了。同時編譯兩個檔案的指令是:cc -c main.c sum.c

編譯成功後,生成了2個.o目标檔案

【C語言】11-函數的聲明和定義

也可以單獨編譯:

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函數的聲明
【C語言】11-函數的聲明和定義
  • 如果隻是連結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

【C語言】11-函數的聲明和定義

3> 運作a.out檔案:./a.out,運作結果是在螢幕上輸出了:

c is 21      

說明函數調用成功,我們已經成功在main.c檔案的main函數中調用了sum.c檔案中的sum函數

4> 從中也可以得出一個結論:隻要知道某個函數的聲明,就可以調用這個函數,編譯就能成功。不過想要這個程式能夠運作成功,必須保證在連結的時候能找到函數的定義。

三、#include

了解完前面的知識後,接下來就可以搞懂一個很久以前的問題:每次寫在最前面的#include是幹啥用的?

1.#include的作用

先來看一個最簡單的C程式:

【C語言】11-函數的聲明和定義
1 #include <stdio.h>
2 
3 int main()
4 {
5     printf("Hello, World!\n");
6     return 0;
7 }      
【C語言】11-函數的聲明和定義

這個程式的作用是在螢幕上輸出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中

【C語言】11-函數的聲明和定義

2> 在main.c源檔案中包含my.txt檔案

【C語言】11-函數的聲明和定義
  • 編譯連結後,程式還是可以照常運作的,因為 #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中

【C語言】11-函數的聲明和定義

這是sum.h檔案

【C語言】11-函數的聲明和定義

這是sum.c檔案

然後在main.c中包含sum.h即可使用sum函數

【C語言】11-函數的聲明和定義

其實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檔案了?

【C語言】11-函數的聲明和定義

大家都知道#include的功能是拷貝内容,是以上面的代碼等效于:

【C語言】11-函數的聲明和定義

這麼一看,文法上是絕對沒有問題的,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中聲明的函數。這樣子,張三和李四就可以互相協作、不會沖突。