介紹下extern和標頭檔案的聯絡
導讀 | 如果一個檔案(假設檔名A)要大量引用另一個檔案(假設檔名B)中定義的變數或函式,則使用標頭檔案效率更高,程式結構也更規範。 |
用 #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就只能是定義。注:定義要為變數分配記憶體空間;而宣告不需要為變數分配記憶體空間。
下面分變數和函式兩類來說:
(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"進行連結指定,這告訴編譯器,請保持我的名稱,不要給我生成用於連結的中間名。
這種聯絡也解決了最初提出的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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- C++標準庫標頭檔案介紹C++
- 8.13 標頭檔案剖析:標頭檔案路徑(下)
- extern 用法,全域性變數與標頭檔案(重複定義)變數
- locate標頭檔案和庫檔案
- algorithm標頭檔案下的常用函式Go函式
- linux下使用windows標頭檔案LinuxWindows
- C++基礎::函式、類、型別所在的標頭檔案 && 介面的介紹C++函式型別
- Centos和Redhat有什麼關係?Centos和Redhat的區別與聯絡介紹CentOSRedhat
- C 標頭檔案
- C#專案obj和bin檔案什麼區別和聯絡C#OBJ
- 標頭檔案的作用分析
- C++標準庫名字和標頭檔案--表C++
- #include sys/xxx.h標頭檔案 UNIX標頭檔案
- 02@在類的標頭檔案中儘量少引入其他標頭檔案
- C 標頭檔案 作用
- 祖傳標頭檔案
- podspec檔案介紹
- Tuxedo的配置檔案介紹UX
- C++標頭檔案<algorithm>中常用函式簡介C++Go函式
- gcc g++ 新增標頭檔案路徑和庫檔案路徑的方法GC
- fcntl.h標頭檔案
- linux 標頭檔案 作用Linux
- 什麼是 標頭檔案
- 關於C++的標頭檔案C++
- File、Blob、ArrayBuffer等檔案類的物件有什麼區別和聯絡物件
- CPL檔案利用介紹
- IDEA下JNI開發快速生成標頭檔案方法Idea
- linux和STL 常用標頭檔案及說明Linux
- word製作聯合檔案頭的方法
- C/C++標頭檔案太難記?一個萬能標頭檔案全搞定!C++
- C語言標頭檔案#include的作用C語言
- c++筆記_標頭檔案C++筆記
- Nt函式原型標頭檔案函式原型
- C語言 - 標頭檔案包含C語言
- 簡單介紹ASP.NET Core實現檔案上傳和下載ASP.NET
- 標頭檔案與庫檔案與菜鳥 (轉)
- 模組的封裝(四):標頭檔案的疼封裝
- 檔案管理簡單介紹