解析“extern”(轉)

解析“extern”
1、 聲明外部變量
現代編譯器一般採用按文件編譯的方式,因此在編譯時,各個文件中定義的全局變量是
互相透明的,也就是說,在編譯時,全局變量的可見域限制在文件內部。下面舉一個簡單的例子。創建一個工程,裏面含有A.cpp和B.cpp兩個簡單的C++源文件:
//A.cpp
int i;

void main()
{
}





//B.cpp
int i;



這兩個文件極爲簡單,在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中定義的全局變量
}





//B.cpp
int i;



編譯結果如下:


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中定義的全局變量
}






//B.cpp
int i;



順利通過編譯,鏈接。


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()
{
}




程序的結構是這樣的:在文件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();
}






//B.C
void 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()調用被編譯成了
call ?func1@@YAXXZ


如果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
int i;


程序很簡單:在文件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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章