說明:這個C語言專題,是學習iOS開發的前奏。也為了讓有面向對象語言開發經驗的程序員,能夠快速上手C語言。如果你還沒有編程經驗,或者對C語言、iOS開發不感興趣,請忽略
上一講中大致介紹了變量的類型,不同類型的變量有不同的存儲類型、不同的生命周期、不同的作用域。這講介紹2個比較重要的關鍵字:static和extern。
static和extern不僅可以用在變量上,還可以用在函數上。這講先介紹它們對函數的作用。
一、extern與函數
在第三講和第四講中,我提到過一句話:如果一個程序中有多個源文件(.c),編譯成功會生成對應的多個目標文件(.obj),這些目標文件還不能單獨運行,因為這些目標文件之間可能會有關聯,比如a.obj可能會調用c.obj中定義的一個函數。將這些相關聯的目標文件鏈接在一起后才能生成可執行文件。
先來理解2個概念:
- 外部函數:如果在當前文件中定義的函數允許其他文件訪問、調用,就稱為外部函數。C語言規定,不允許有同名的外部函數。
- 內部函數:如果在當前文件中定義的函數不允許其他文件訪問、調用,只能在內部使用,就稱為內部函數。C語言規定不同的源文件可以有同名的內部函數,並且互不干擾。
接下來就演示在一個源文件中調用另外一個源文件定義的函數,比如在main.c中調用one.c中定義的one函數。
1.首先在one.c中定義了一個one函數
如果你想讓這個one函數可以被main.c訪問,那么one函數就必須是外部函數。完整的定義是要加上extern關鍵字。
不過這個extern跟auto關鍵字一樣廢,完全可以省略,因為默認情況下,所有的函數就是外部函數。我們可以簡化一下:
2.接下來,我想在main.c的main函數中,調用one.c中的one函數
怎樣才能調用one.c中的one函數呢?你可能會產生2個想法:
想法1:直接在main函數中寫上one();
這個做法肯定不行,因為main函數根本不知道one函數的存在,怎么調用呢?這個在標准C編譯器里面會報錯的,但是在Xcode中只是個警告。
想法2:在main.c中包含one.c文件
大家都知道#include的作用純粹就是內容拷貝,所以又相當於
哎,這么一看好像是對的哦,在main函數前面定義了個one函數,然后在main函數中調用了這個one函數。從語法上看是對的,所以編譯是沒問題的。但是這個程序不可能運行成功,因為在鏈接的時候會報錯。我們已經在one.c中定義了one函數,現在又在main.c中定義one函數,C語言規定不允許有同名的外部函數,鏈接的時候鏈接器會發現在one.obj和main.obj中定義了同一個函數,會直接報錯,Xcode中的錯誤信息是這樣的:
duplicate symbol _one是說one這個標識符重復了,linker是指鏈接器。
上面的2種想法都是不可行的,其實思路是一致的:讓main函數知道one函數的存在。正確的做法應該是在main函數前面對one函數進行提前聲明(看清楚,是聲明,不是定義,定義和聲明是兩碼事)。
3.在main函數前面對one函數進行提前聲明
你想要把其他源文件中定義的外部函數拿過來聲明,完整的做法,應該使用extern關鍵字,表示引用別人的"外部函數"
運行程序,從控制台輸出可以發現 "one.c中定義的one函數" 已經被 "main.c的main函數" 成功調用了。
也有人可能會馬上冒出一個想法:假如除開one.c,還有其他源文件也有定義這個one函數怎么辦?那main函數調用的究竟是誰的one函數啊?放心,絕對不會有這種情況,剛才不是說了么,不允許重復定義同一個外部函數,不然鏈接器會報錯的,所以只會有一個外部one函數。
上述就是extern關鍵字對函數的作用:用來定義和聲明一個外部函數。其實extern又跟auto一樣廢,完全可以省略。於是,我們可以簡化成這樣:
為了模塊化地開發,在正規的項目里面,我們會把one函數的聲明寫到另一個頭文件中,當然,這個頭文件的命名最好有意義、規范一點,比如叫one.h。以后,誰想調用這個one函數,包含one.h這個頭文件就行了。於是最后的代碼結構是這樣的:
二、static與函數
1.定義內部函數
從上面的例子可以看出,one.c中定義的one函數是可以被其他源文件訪問的。其實有時候,我們可能想定義一個"內部函數",也就是不想讓其他文件訪問本文件中定義的函數。這個非常簡單,你只需要在定義函數的時候加個static關鍵字即可。
(我們就在上面例子的代碼基礎上進行修改)
我在void one()的前面加了個static,代表one函數是個內部函數。
然后你會發現程序運行不起來了,在鏈接的時候就報錯了。報錯的原因很簡單:我們在main.c中調用了one.c中定義的one函數,但是現在one.c的one函數是個"內部函數",不允許其他文件訪問。我們來看看錯誤信息:
第1個紅框中的Undefined symbols...意思是one這個標識符沒有被定義,也就是找不到one;第2個紅框的linker表明是鏈接器報錯了。
但這個程序是可以編譯成功的,因為我們在main函數前面聲明了one函數(函數的聲明和定義是兩碼事),這個函數聲明可以理解為:在語法上,騙一下main函數,告訴它one函數是存在的,所以從語法的角度上main函數是可以調用one函數的。究竟這個one函數存不存在呢,有沒有被定義呢?編譯器是不管的。在編譯階段,編譯器只會檢測單個源文件的語法合不合理,並不檢測函數有沒有定義,只有在鏈接的時候才會檢測這個函數存不存在,也就是有沒有被定義。
我們再來討論一個問題,為什么好多情況下都是可以成功編譯,但是鏈接的時候報錯呢?只要你理解編譯和鏈接的作用就好辦了。
所謂編譯,就是單獨檢查每個源文件的語法是否合理,並不會檢查每個源文件之間的關聯關系,一個源文件編譯成功就生成一個目標文件。
所謂鏈接,就是檢查目標文件的關聯關系,將相關聯的目標文件組合在一起,生成可執行文件。
看完這2個概念,再回去思考下前面報的錯,應該可以完全明白了。
2.聲明內部函數
我們還可以用static聲明一個內部函數
1 #include <stdio.h> 2 3 static void test(); 4 5 int main(int argc, const char * argv[]) 6 { 7 test(); 8 return 0; 9 } 10 11 static void test() { 12 printf("調用了test函數"); 13 }
在第11行定義了一個test函數,這是一個內部函數,接着在第3行對test函數進行提前聲明,然后就可以在第7行可以調用test()函數了
三、static、extern與函數的總結
1.static
* 在定義函數時,在函數的最左邊加上static可以把該函數聲明為內部函數(又叫靜態函數),這樣該函數就只能在其定義所在的文件中使用。如果在不同的文件中有同名的內部函數,則互不干擾。
* static也可以用來聲明一個內部函數
2.extern
* 在定義函數時,如果在函數的最左邊加上關鍵字extern,則表示此函數是外部函數,可供其他文件調用。C語言規定,如果在定義函數時省略extern,則隱含為外部函數。
* 在一個文件中要調用其他文件中的外部函數,則需要在當前文件中用extern聲明該外部函數,然后就可以使用,這里的extern也可以省略。