1. 程式人生 > 其它 >C/C++中extern關鍵字

C/C++中extern關鍵字

一.基本解釋

  • extern關鍵字可以置於變數或者函式前,以標示變數或者函式的定義在別的檔案中,提示編譯器遇到此變數和函式時在其他模組中尋找其定義。所以一般extern關鍵字後面跟著的都是宣告
  • 它有兩個作用:
  1. 當extern與"C"一起連用時,如:extern "C" void fun(int a, int b); 這就告訴編譯器在編譯fun這個函式名時按著C的規則去翻譯相應的函式名而不是C++的,C++的規則在翻譯這個函式名時會把fun這個名字弄得面目全非,例如:fun@aBc_isdnfsdf#%&,不同編譯器採取的翻譯方法不一樣。這麼做的原因因為C++支援函式過載
    下面是一個標準的寫法:
#ifdef __cplusplus
#if __cplusplus
extern "C"{
    #endif
    #endif /*__cplusplus*/
}
#endif
#endif /*__cplusplus*/
  1. 當extern不與"C"一起修飾變數或函式,例如在標頭檔案中:extern int f_int; 它的作用就是宣告函式全域性變數作用範圍的關鍵字,其宣告的函式和變數可以在本模組或其他模組中使用,一定要切記是宣告
    例:當B模組(編譯單元)引用A模組(編譯單元)中定義的全域性變數或函式時,它只要包含A模組的標頭檔案即可,在編譯階段,模組B雖然找不到該函式或變數,但不會報錯,它會在連結
    時從模組A生成的目的碼中找到該函式。

二.當extern修飾變數時,定義式要嚴格對應宣告時的格式

  • 例如:在原始檔定義了一個數組:char a[666]; 標頭檔案中宣告的格式卻是:extern char *a; 會造成非法訪問,原因在於指向型別T的指標並不等價於型別T的陣列。應將宣告改為extern char a[];
  • 當你在.cpp檔案中定義了一個全域性的變數,這個全域性變數如果要被引用,就放到.h中並用extern宣告。

三.單方面修改extern函式原型

  • 當函式提供方單方面修改函式原型時,若使用方不知情而繼續沿用原來的extern宣告,這樣編譯時編譯器不會報錯。但執行過程中,由於輸入引數的不匹配,往往會造成系統錯誤。
  • 處理這個問題目前通常的做法是函式提供方在自己的.h中提供對外部介面的宣告,然後函式呼叫方include該標頭檔案,從而省去extern這一步。

四.extern函式宣告

  • 如果函式宣告中帶有關鍵字extern,僅僅暗示這個函式可能在其他原始檔裡定義,無其他作用。extern int f();與int f();沒有明顯區別。
  • 它的用處在於在程式中取代include "*.h"來宣告函式,例如:
    (1)在test1.h中有下列宣告
#ifdef TEST1H
#define TEST1H
extern char g_str[];//宣告全域性變數
void fun1();
#endif

(2)在test1.cpp中

#include "test1.h"
    char g_str[] = "123456";//定義全域性變數
    void fun1(){cout << g_str << endl;} 

(3)以上的test1模組,編譯和連結都可以通過,如果我們還有test2模組且也想使用g_str,只需要在.cpp中引用就可以:

#include "test1.h"
void fun2(){cout << g_str << endl;}

此時若開啟test1.obj,你可以在裡面找到"123456"這個字串,但是你卻不能在test2.obj中找到它,因為g_str為整個工程專案的全域性變數,在記憶體中只存在一份,如果test2.obj也有一份,會在連結時報告重複定義錯誤。
(4)當全域性變數宣告與定義放在一起時,例如把test1.h改為:
extern char g_str[] = "123456";//此時相當於無extern
然後令test1.cpp中的定義去掉,此時再編譯連結test1和test2兩個模組時,會報連結錯誤,這是因為全域性變數g_str的定義放到了標頭檔案中,test1.cpp與test2.cpp同時包含test1.h,此時連結器在連結test1和test2時發現了兩個g_str,造成重複定義。
解決這個問題的辦法是:把test2的程式碼中的#include "test1.h"去掉換成
extern char g_str[];
void fun2() {cout << g_str << endl;}
這時編譯器就知道g_str是引自於外部的一個編譯模組不會在本模組再重複定義一個出來,但是這麼做其實非常糟糕,因為你由於無法在test2.cpp中使用#include "test1.h",那麼test1.h宣告的其他函式你也無法使用,除非都用extern宣告,你會寫一大堆無效程式碼且這與標頭檔案對外提供介面的初衷背離,所以,謹記:在標頭檔案中只做宣告

五.extern與static

  • extern表示該變數在其他地方已經定義過了,在這裡要使用那個變數。
  • static表示靜態變數,分配記憶體時儲存在靜態區,不儲存在棧上。
    static作用範圍是內部連線的關係,和extern有點相反;它和物件本身是分開儲存的,extern也是分開儲存,但extern可以被其他物件用extern引用,但static只允許物件本身用它。它倆的區別:
    1.extern和static不能同時修飾一個變數。
    2.static修飾的全域性變數宣告與定義同時進行,也就是說,當你在標頭檔案中使用static宣告全域性變數後,它也同時被定義了。
    3.static修飾的全域性變數的作用域只能是本身編譯單元,也就是說它的“全域性”只對本編譯單元有效,其他編譯單元看不到它,例如:
    (1)test1.h:
#ifndef TEST1H
#define TEST1H
static cahr g_str[] = "123456";
void fun1();
#endif

(2)test1.cpp:

#include "test1.h"
void fun1(){cout << g_str << endl;}

(3)test2.cpp:

void fun2(){cout << g_str << endl; }

以上兩個編譯單元可以連結成功,當你開啟test1.obj時,可以在其中找到字串“123456”,同時,也能在test2.obj中找到他們,之所以能連結成功而沒有報重複定義,是因為雖然他們有相同的內容,但是儲存的實體地址不同,就像兩個不同的變數賦給了相同的值,而這兩個變數分別作用於它們各自的編譯單元。
此時如果你想探查到底,跟蹤除錯上述程式碼,結果你可以會發現兩個編譯單元的g_str的記憶體地址相同。這可與我們總結的嚴重不符,原因在哪呢?其實這是編譯器在欺騙你,大多數編譯器都有程式碼優化功能,以達成生成的目標程式更節省記憶體,執行效率更高,當編譯器在連線各個單元的時候,它會把相同內容的記憶體只拷貝一份,就例如上述的“123456”。
下列的程式碼能夠馬上拆穿這個謊言:
(1)test1.cpp:

#include "test1.h"
void fun1(){
    g_str[0] = 'a';
    cout << g_str << endl;
}

(2)test2.cpp:

#include "test1.h"
void fun2(){cout << g_str << endl;}

(3)main.cpp:

void main(){
    fun1();//a23456
    fun2();//123456
}

此時在跟蹤程式碼時,就會發現兩個編譯單元中的g_str地址並不相同,因為你在一處修改了它,所以編譯器強行恢復記憶體的原貌,在記憶體中存在了兩份拷貝給兩個模組。正因為static有這樣的特性,所以一般定義static全域性變數時,都把它放在原始檔而不是標頭檔案,這樣就不會給其他模組造成不必要的資訊汙染。

六.extern和const

const可以與extern連用來宣告該常量可以作用於其他編譯模組中,標頭檔案中extern const char g_str[];,然後在原始檔定義const char g_str[] = "123456"
所以當const單獨使用時與static相同,而當與extern連用時,特性就跟extern的一樣了。

內容總結於博主chao_yu