介紹下extern和標頭檔案的聯絡

大雄45發表於2021-06-07
導讀 如果一個檔案(假設檔名A)要大量引用另一個檔案(假設檔名B)中定義的變數或函式,則使用標頭檔案效率更高,程式結構也更規範。

介紹下extern和標頭檔案的聯絡介紹下extern和標頭檔案的聯絡

用 #include 可以包含其他標頭檔案中變數、函式的宣告,為什麼還要 extern 關鍵字?

如果我想引用一個全域性變數或函式a,我只要直接在原始檔中包含 #include (xxx.h包含了a的宣告)不就可以了麼,為什麼還要用 extern 呢?

這個問題一直也是似是而非的困擾著我許久,經過實踐和查詢資料,有如下總結:

一、標頭檔案

首先說下標頭檔案,其實標頭檔案對計算機而言沒什麼作用,她只是在預編譯時在 #include 的地方展開一下,沒別的意義了,其實標頭檔案主要是給別人看的。

我做過一個實驗,將標頭檔案的字尾改成 xxx.txt,然後在引用該標頭檔案的地方用 #include "xxx.txt", 編譯,連結都很順利的過去了,由此可知,標頭檔案僅僅為閱讀程式碼作用,沒其他的作用了!

不管是C還是C++,你把你的函式,變數或者結構體,類啥的放在你的.c或者.cpp檔案裡。然後編譯成lib,dll,obj,.o等等,然後別人用的時候,最基本的gcc hisfile.cpp yourfile.o|obj|dll|lib 等等。

但對於我們程式設計師而言,他們怎麼知道你的lib,dll...裡面到底有什麼東西?要看你的標頭檔案。你的標頭檔案就是對使用者的說明。函式,引數,各種各樣的介面的說明。

那既然是說明,那麼標頭檔案裡面放的自然就是關於函式,變數,類的"宣告"(對函式來說,也叫函式原型)了。記著,是"宣告",不是"定義"。

那麼,我假設大家知道宣告和定義的區別。所以,最好不要傻嘻嘻的在標頭檔案裡定義什麼東西。比如全域性變數:

/*xx標頭檔案*/
#ifndef _XX_標頭檔案.H
#define _XX_標頭檔案.H
int A;
#endif

那麼,很糟糕的是,這裡的 int A 是個全域性變數的定義,所以如果這個標頭檔案被多次引用的話,你的A會被重複定義,顯然語法上錯了。只不過有了這個 #ifndef 的條件編譯,所以能保證你的標頭檔案只被引用一次,不過也許還是不會出岔子,但若多個 c 檔案包含這個標頭檔案時還是會出錯的,因為宏名有效範圍僅限於本c原始檔,所以在這多個 c 檔案編譯時是不會出錯的,但在連結時就會報錯,說你多處定義了同一個變數:

Linking...
incl2.obj : error LNK2005: "int glb" (?glb@@3HA) already defined in incl1.obj
Debug/incl.exe : fatal error LNK1169: one or more multiply defined symbols found
二、extern

這個關鍵字真的比較可惡,在定義變數的時候,這個extern居然可以被省略(定義時,預設均省略);在宣告變數的時候,這個extern必須新增在變數前,所以有時會讓你搞不清楚到底是宣告還是定義。或者說,變數前有extern不一定就是宣告,而變數前無extern就只能是定義。注:定義要為變數分配記憶體空間;而宣告不需要為變數分配記憶體空間。

下面分變數和函式兩類來說:

(1)變數

尤其是對於變數來說:

extern int a; // 宣告一個全域性變數 a
int a; // 定義一個全域性變數 a
extern int a =0 ; // 定義一個全域性變數 a 並給初值。
int a =0;    // 定義一個全域性變數 a, 並給初值,

第四個等於第三個,都是定義一個可以被外部使用的全域性變數,並給初值。

糊塗了吧,他們看上去可真像。但是定義只能出現在一處。也就是說,不管是int a;還是extern int a=0;還是int a=0;都只能出現一次,而那個extern int a可以出現很多次。

當你要引用一個全域性變數的時候,你就必須要宣告,extern int a; 這時候extern不能省略,因為省略了,就變成int a;這是一個定義,不是宣告。注:extern int a; 中型別 int 可省略,即 extern a; 但其他型別則不能省略。

(2)函式

函式,對於函式也一樣,也是定義和宣告,定義的時候用extern,說明這個函式是可以被外部引用的,宣告的時候用extern說明這是一個宣告。 但由於函式的定義和宣告是有區別的,定義函式要有函式體,宣告函式沒有函式體(還有以分號結尾),所以函式定義和宣告時都可以將extern省略掉,反正其他檔案也是知道這個函式是在其他地方定義的,所以不加extern也行。兩者如此不同,所以省略了extern也不會有問題。

比如:

/*某cpp檔案*/
int fun(void)
{
      return 0;
}

很好,我們定義了一個全域性函式:

/*另一cpp檔案*/
int fun(void);

我們對它做了個宣告,然後後面就可以用了, 加不加extern都一樣, 我們也可以把對 fun 的宣告放在一個標頭檔案裡,最後變成這樣:

/*fun.h*/
int fun(void);   //函式宣告,所以省略了extern,完整些是extern int fun(void);
/*對應的fun.cpp檔案*/
int fun(void)
{
     return 0;
}//一個完整的全域性函式定義,因為有函式體,extern同樣被省略了。

然後,一個客戶,一個要使用你的fun的客戶,把這個標頭檔案包含進去,ok,一個全域性的宣告。沒有問題。

但是,對應的,如果是這個客戶要使用全域性變數,那麼要extern 某某變數;不然就成了定義了。

總結:
對變數而言,如果你想在本原始檔(例如檔名A)中使用另一個原始檔(例如檔名B)的變數,方法有2種:(1)在A檔案中必須用extern宣告在B檔案中定義的變數(當然是全域性變數);(2)在A檔案中新增B檔案對應的標頭檔案,當然這個標頭檔案包含B檔案中的變數宣告,也即在這個標頭檔案中必須用extern宣告該變數,否則,該變數又被定義一次。
對函式而言,如果你想在本原始檔(例如檔名A)中使用另一個原始檔(例如檔名B)的函式,方法有2種:(1)在A檔案中用extern宣告在B檔案中定義的函式(其實,也可省略extern,只需在A檔案中出現B檔案定義函式原型即可);(2)在A檔案中新增B檔案對應的標頭檔案,當然這個標頭檔案包含B檔案中的函式原型,在標頭檔案中函式可以不用加extern。

對上述總結換一種說法:

(a)對於一個檔案中呼叫另一個檔案的全域性變數,因為全域性變數一般定義在原檔案.c中,我們不能用#include包含原始檔而只能包含標頭檔案,所以常用的方法是用extern int a來宣告外部變數。 另外一種方法是可以是在a.c檔案中定義了全域性變數int global_num ,可以在對應的a.h標頭檔案中寫extern int global_num ,這樣其他原始檔可以透過include a.h來宣告她是外部變數就可以了。

(b)還有變數和函式的不同舉例 int fun(); 和 extern int fun(); 都是宣告(定義要有實現體)。 用 extern int fun() 只是更明確指明是宣告而已。而 int a; 是定義 extern int a; 是宣告。

(3)此外,extern修飾符可用於C++程式中呼叫c函式的規範問題。

比如在C++中呼叫C庫函式,就需要在C++程式中用extern "C"宣告要引用的函式。這是給連結器用的,告訴連結器在連結的時候用C函式規範來連結。主要原因是C++和C程式編譯完成後在目的碼中命名規則不同。

C++語言在編譯的時候為了解決的多型問題,會將名和引數聯合起來生成一箇中間的名稱,而c語言則不會,因此會造成連結時找不到對應的情況,此時C就需要用extern "C"進行連結指定,這告訴編譯器,請保持我的名稱,不要給我生成用於連結的中間名。

三、extern和標頭檔案的聯絡

這種聯絡也解決了最初提出的2個問題:

(a)用#include可以包含其他標頭檔案中變數、函式的宣告,為什麼還要extern關鍵字?

(b)如果我想引用一個全域性變數或函式a,我只要直接在原始檔中包含#include (xxx.h包含了a的宣告)不就可以了麼,為什麼還要用extern呢??

答案:如果一個檔案(假設檔名A)要大量引用另一個檔案(假設檔名B)中定義的變數或函式,則使用標頭檔案效率更高,程式結構也更規範。其他檔案(例如檔名C、D等)要引用檔名B中定義的變數或函式,則只需用#include包含檔案B對應的標頭檔案(當然,這個標頭檔案只有對變數或函式的宣告,絕不能有定義)即可。

那是一個被遺忘的年代,那時,編譯器只認識.c(或.cpp)檔案,而不知道.h是何物的年代。

那時的人們寫了很多的.c(或.cpp)檔案,漸漸地,人們發現在很多.c(或.cpp)檔案中的宣告變數或函式原型是相同的,但他們卻不得不一個字一個字地重複地將這些內容敲入每個.c(或.cpp)檔案。但更為恐怖的是,當其中一個宣告有變更時,就需要檢查所有的.c(或.cpp)檔案,並修改其中的宣告,啊~,簡直是世界末日降臨!

終於,有人(或許是一些人)再不能忍受這樣的折磨,他(們)將重複的部分提取出來,放在一個新檔案裡,然後在需要的.c(或.cpp)檔案中敲入#include XXXX這樣的語句。這樣即使某個宣告發生了變更,也再不需要到處尋找與修改了---世界還是那麼美好!

因為這個新檔案,經常被放在.c(或.cpp)檔案的頭部,所以就給它起名叫做"標頭檔案",副檔名是.h。

從此,編譯器(其實是其中前處理器)就知道世上除了.c(或.cpp)檔案,還有個.h的檔案,以及一個叫做#include 。

原文來自:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2775856/,如需轉載,請註明出處,否則將追究法律責任。

相關文章