link:
http://blog.csdn.net/keensword/archive/2005/06/23/401114.aspx
解析“extern”
1、 聲明外部變量
現代編譯器一般采用按文件編譯的方式,因此在編譯時,各個文件中定義的全局變量是
互相透明的,也就是說,在編譯時,全局變量的可見域限制在文件內部。下面舉一個簡單的例子。創建一個工程,里面含有A.cpp和B.cpp兩個簡單的C++源文件:
//A.cpp
int i;
void main()
{
}
|
這兩個文件極為簡單,在A.cpp中我們定義了一個全局變量i,在B中我們也定義了一個全局變量i。
我們對A和B分別編譯,都可以正常通過編譯,但是進行鏈接的時候,卻出現了錯誤,錯誤提示如下:
Linking...
B.obj : error LNK2005: "int i" (?i@@3HA) already defined in A.obj
Debug/A.exe : fatal error LNK1169: one or more multiply defined symbols found
Error executing link.exe.
A.exe - 2 error(s), 0 warning(s)
|
這就是說,在編譯階段,各個文件中定義的全局變量相互是透明的,編譯A時覺察不到B中也定義了i,同樣,編譯B時覺察不到A中也定義了i。
但是到了鏈接階段,要將各個文件的內容“合為一體”,因此,如果某些文件中定義的全局變量名相同的話,在這個時候就會出現錯誤,也就是上面提示的重復定義的錯誤。
因此,各個文件中定義的全局變量名不可相同。
在鏈接階段,各個文件的內容(實際是編譯產生的obj文件)是被合并到一起的,因而,定義于某文件內的全局變量,在鏈接完成后,它的可見范圍被擴大到了整個程序。
這樣一來,按道理說,一個文件中定義的全局變量,可以在整個程序的任何地方被使用,舉例說,如果A文件中定義了某全局變量,那么B文件中應可以該變量。修改我們的程序,加以驗證:
//A.cpp
void main()
{
i = 100; //試圖使用B中定義的全局變量
}
|
編譯結果如下:
Compiling...
A.cpp
C:\Documents and Settings\wangjian\桌面\try extern\A.cpp(5) : error C2065: 'i' : undeclared identifier
Error executing cl.exe.
A.obj - 1 error(s), 0 warning(s)
|
編譯錯誤。
其實出現這個錯誤是意料之中的,因為:文件中定義的全局變量的可見性擴展到整個程序是在鏈接完成之后,而在編譯階段,他們的可見性仍局限于各自的文件。
編譯器的目光不夠長遠,編譯器沒有能夠意識到,某個變量符號雖然不是本文件定義的,但是它可能是在其它的文件中定義的。
雖然編譯器不夠遠見,但是我們可以給它提示,幫助它來解決上面出現的問題。這就是extern的作用了。
extern的原理很簡單,就是告訴編譯器:“你現在編譯的文件中,有一個標識符雖然沒有在本文件中定義,但是它是在別的文件中定義的全局變量,你要放行!”
我們為上面的錯誤程序加上extern關鍵字:
//A.cpp
extern int i;
void main()
{
i = 100; //試圖使用B中定義的全局變量
}
|
順利通過編譯,鏈接。
2、 在C++文件中調用C方式編譯的函數
C方式編譯和C++方式編譯
相對于C,C++中新增了諸如重載等新特性,對于他們的編譯,必然有一些重要的區別。
我們將下面的小程序分別按C和C++方式編譯,來探討兩種編譯方式的區別。
int i;
int func(int t)
{
return 0;
}
void main()
{
}
|
以C方式編譯的結果:
COMM _i : DWORD
PUBLIC _func
PUBLIC _main
|
以C++方式編譯的結果:
PUBLIC ?i@@3HA ; i
PUBLIC ?func@@YAHH@Z ; func
PUBLIC _main
|
可見,C方式編譯下,變量名和函數名之前被統一加上了一個下劃線,而C++編譯后的結果卻復雜的多,i變成了?i@@3HA,func變成了?func@@YAHH@Z。C++中的這種看似復雜的命名規則是為C++中的函數重載,參數檢查等特性服務的。
多文件程序中的函數調用
一般情況下,工程中的文件都是CPP文件(以及頭文件)。如下面的程序僅包含兩個文件:A.CPP和B.CPP:
//A.CPP
void func();
void main()
{
func();
}
|
程序的結構是這樣的:在文件B.CPP中定義了一個函數void func(),main函數位于文件A.CPP,在main函數中調用了B中定義的函數func()。
要在A中調用B中定義的函數,必須要加上該函數的聲明。如本例中的void func();就是對函數func()的聲明。
如果沒有聲明的話,編譯A.CPP時就會出錯。因為編譯器的目光只局限于被編譯文件,必須通過加入函數聲明來告訴編譯器:“某個函數是定義在其它的文件中的,你要放行!”,這一點跟用extern來聲明外部全局變量是一個道理。
需要注意的是,一般的程序都是通過包含頭文件來完成函數的聲明。拿本例來說,一般是創建一個頭文件B.H,在頭文件中加入聲明語句void func(); 并且在A.CPP中加入包含語句:#include “B.H”。
在C++程序中,頭文件的功能從函數聲明被擴展為類的定義。
不同編譯方式下的函數調用
如果在工程中,不僅有CPP文件,還有以C方式編譯的C文件,函數調用就會有一些微妙之處。我們將B.CPP改作B.C:
//A.CPP
void func();
void main()
{
func();
}
|
對A.CPP和B.C分別編譯,都沒有問題,但是鏈接時出現錯誤。
Linking...
A.obj : error LNK2001: unresolved external symbol "void __cdecl func(void)" (?func@@YAXXZ)
Debug/A.exe : fatal error LNK1120: 1 unresolved externals
Error executing link.exe.
A.exe - 2 error(s), 0 warning(s)
|
原因就在于不同的編譯方式產生的沖突。
對于文件A,是按照C++的方式進行編譯的,其中的func()調用被編譯成了
如果B文件也是按照C++方式編譯的,那么B中的func函數名也會被編譯器改成?func1@@YAXXZ,這樣的話,就沒有任何問題。
但是現在對B文件,是按照C方式編譯的,B中的func函數名被改成了_func,這樣一來,A中的call ?func1@@YAXXZ這個函數調用就沒有了著落,因為在鏈接器看來,B文件中沒有名為?func1@@YAXXZ的函數。
事實是,我們編程者知道,B文件中有A中調用的func函數的定義,只不過它是按照C方式編譯的,故它的名字被改成了_func。因而,我們需要通過某種方式告訴編譯器:“B中定義的函數func()經編譯后命名成了_func,而不是?func1@@YAXXZ,你必須通過call _func來調用它,而不是call ?func1@@YAXXZ。”簡單的說,就是告訴編譯器,調用的func()函數是以C方式編譯的,fun();語句必須被編譯成call _func;而不是call ?func1@@YAXXZ。
我們可以通過extern關鍵字,來幫助編譯器解決上面提到的問題。
對于本例,只需將A.CPP改成如下即可:
//A.CPP
extern "C"
{
void func();
}
void main()
{
func();
}
|
察看匯編代碼,發現此時的func();語句被編譯成了call _func。
3、 補充
同2一樣,仍然是C,C++混合編程的情形,考慮下面的程序:
//A.CPP
extern int i;
void main()
{
i = 100;
}
|
程序很簡單:在文件B.C中定義了一個全局變量i,在A.CPP中使用了這個全局變量。
編譯沒有問題,鏈接時卻出現錯誤:
Linking...
A.obj : error LNK2001: unresolved external symbol "int i" (?i@@3HA)
Debug/A.exe : fatal error LNK1120: 1 unresolved externals
Error executing link.exe.
A.exe - 2 error(s), 0 warning(s)
|
這是因為,在C方式編譯下,i被重命名為_i,而在C++方式下,i會被重命名為?i@@3HA。
因而,我們只用extern int i;來聲明還不夠,必須告訴編譯器,全局變量i是以C方式編譯的,
它會被重命名為_i,而不是?i@@3HA。
我們修改A.CPP,如下:
//A.CPP
extern "C"
{
int i;
}
void main()
{
i = 100;
}
|
程序正常通過編譯和鏈接。
我們察看一下匯編代碼,發現語句i = 100;被編譯成了mov DWORD PTR _i, 100。