2010年1月1日 星期五

extern 和 C/C++,C++技術文章,C++系列教程,C++

extern 和 C/C++,C++技術文章,C++系列教程,C++

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。

沒有留言: