天天看點

extern "C"用法解析

C++ 中 extern “C” 含義深層探索                                       1. 引言    C++ 語言的建立初衷是 “a better C” ,但是這并不意味着 C++ 中類似 C 語言的全局變量和函數所采用的編譯和連接配接方式與 C 語言完全相同。作為一種欲與 C 相容的語言, C++ 保留了一部分過程式語言的特點(被世人稱為 “ 不徹底地面向對象 ” ),因而它可以定義不屬于任何類的全局變量和函數。但是, C++ 畢竟是一種面向對象的程式設計語言 ,為了支援函數的重載, C++ 對全局函數的處理方式與 C 有明顯的不同。 2. 從标準頭檔案說起   某企業曾經給出如下的一道面試題:   面試題   為什麼标準頭檔案都有類似以下的結構?     #ifndef __INCvxWorksh     #define __INCvxWorksh     #ifdef __cplusplus     extern "C" {     #endif         #ifdef __cplusplus     }     #endif     #endif   分析   顯然,頭檔案中的編譯宏 “#ifndef __INCvxWorksh 、 #define __INCvxWorksh 、 #endif” 的作用是防止該頭檔案被重複引用。   那麼 #ifdef __cplusplus extern "C" {   #endif   #ifdef __cplusplus } #endif   的作用又是什麼呢?我們将在下文一一道來。 3. 深層揭密 extern "C"    extern "C" 包含雙重含義,從字面上即可得到:首先,被它修飾的目标是 “extern” 的;其次,被它修飾的目标是 “C” 的。讓我們來詳細解讀這兩重含義。   被 extern "C" 限定的函數或變量是 extern 類型的;   extern 是 C/C++ 語言中表明函數和全局變量作用範圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本子產品或其它子產品中使用。記住,下列語句:    extern int a;   僅僅是一個變量的聲明,其并不是在定義變量 a ,并未為 a 配置設定記憶體空間。變量 a 在所有子產品中作為一種全局變量隻能被定義一次,否則會出現連接配接錯誤。   通常,在子產品的頭檔案中對本子產品提供給其它子產品引用的函數和全局變量以關鍵字 extern 聲明 。例如,如果子產品 B 欲引用該子產品 A 中定義的全局變量和函數時隻需包含子產品 A 的頭檔案即可。這樣,子產品 B 中調用子產品 A 中的函數時,在編譯階段,子產品 B 雖然找不到該函數,但是并不會報錯;它會在連接配接階段中從子產品 A 編譯生成的目标代碼中找到此函數。   與 extern 對應的關鍵字是 static ,被它修飾的全局變量和函數隻能在本子產品中使用。是以,一個函數或變量隻可能被本子產品使用時,其不可能被 extern “C” 修飾。   被 extern "C" 修飾的變量和函數是按照 C 語言方式編譯和連接配接的;   未加 extern “C” 聲明時的編譯方式   首先看看 C++ 中對類似 C 的函數是怎樣編譯的。   作為一種面向對象的語言, C++ 支援函數重載,而過程式語言 C 則不支援。函數被 C++ 編譯後在符号庫中的名字與 C 語言的不同。例如,假設某個函數的原型為: void foo( int x, int y );   該函數被 C 編譯器編譯後在符号庫中的名字為 _foo ,而 C++ 編譯器則會産生像 _foo_int_int 之類的名字(不同的編譯器可能生成的名字不同,但是都采用了相同的機制,生成的新名字稱為 “mangled name” )。    _foo_int_int 這樣的名字包含了函數名、函數參數數量及類型資訊, C++ 就是靠這種機制來實作函數重載的。例如,在 C++ 中,函數 void foo( int x, int y ) 與 void foo( int x, float y ) 編譯生成的符号是不相同的,後者為 _foo_int_float 。   同樣地, C++ 中的變量除支援局部變量外,還支援類成員變量和全局變量。使用者所編寫程式的類成員變量可能與全局變量同名,我們以 "." 來區分。而本質上,編譯器在進行編譯時,與函數的處理相似,也為類中的變量取了一個獨一無二的名字,這個名字與使用者程式中同名的全局變量名字不同。   未加 extern "C" 聲明時的連接配接方式   假設在 C++ 中,子產品 A 的頭檔案如下: // 子產品 A 頭檔案  moduleA.h #ifndef MODULE_A_H #define MODULE_A_H int foo( int x, int y ); #endif   在子產品 B 中引用該函數: // 子產品 B 實作檔案  moduleB.cpp #include "moduleA.h" foo(2,3);   實際上,在連接配接階段,連接配接器會從子產品 A 生成的目标檔案 moduleA.obj 中尋找 _foo_int_int 這樣的符号!   加 extern "C" 聲明後的編譯和連接配接方式   加 extern "C" 聲明後,子產品 A 的頭檔案變為: // 子產品 A 頭檔案  moduleA.h #ifndef MODULE_A_H #define MODULE_A_H extern "C" int foo( int x, int y ); #endif   在子產品 B 的實作檔案中仍然調用 foo( 2,3 ) ,其結果是:   ( 1 )子產品 A 編譯生成 foo 的目标代碼時,沒有對其名字進行特殊處理,采用了 C 語言的方式;   ( 2 )連接配接器在為子產品 B 的目标代碼尋找 foo(2,3) 調用時,尋找的是未經修改的符号名 _foo 。   如果在子產品 A 中函數聲明了 foo 為 extern "C" 類型,而子產品 B 中包含的是 extern int foo( int x, int y ) ,則子產品 B 找不到子產品 A 中的函數;反之亦然。   是以,可以用一句話概括 extern “C” 這個聲明的真實目的(任何語言中的任何文法特性的誕生都不是随意而為的,來源于真實世界的需求驅動。我們在思考問題時,不能隻停留在這個語言是怎麼做的,還要問一問它為什麼要這麼做,動機是什麼,這樣我們可以更深入地了解許多問題):   實作 C++ 與 C 及其它語言的混合程式設計。 明白了 C++ 中 extern "C" 的設立動機,我們下面來具體分析 extern "C" 通常的使用技巧。    4.extern "C" 的慣用法   ( 1 )在 C++ 中引用 C 語言中的函數和變量,在包含 C 語言頭檔案(假設為 cExample.h )時,需進行下列處理: extern "C" { #include "cExample.h" }   而在 C 語言的頭檔案中,對其外部函數隻能指定為 extern 類型, C 語言中不支援 extern "C" 聲明,在 .c 檔案中包含了 extern "C" 時會出現編譯文法錯誤 。   筆者編寫的 C++ 引用 C 函數例子工程中包含的三個檔案的源代碼如下: #ifndef C_EXAMPLE_H #define C_EXAMPLE_H extern int add(int x,int y);     //注:寫成extern "C" int add(int , int ); 也可以 #endif #include "cExample.h" int add( int x, int y ) {   return x + y; } // c++ 實作檔案,調用 add : cppFile.cpp extern "C" {   #include "cExample.h"        //注:此處不妥,如果這樣編譯通不過,換成 extern "C" int add(int , int ); 可以通過 } int main(int argc, char* argv[]) {   add(2,3);   return 0; }   如果 C++ 調用一個 C 語言編寫的 .DLL 時,當包括 .DLL 的頭檔案或聲明接口函數時,應加 extern "C" {   } 。   ( 2 )在 C 中引用 C++ 語言中的函數和變量時, C++ 的頭檔案需添加 extern "C" ,但是在 C 語言中不能直接引用聲明了 extern "C" 的該頭檔案,應該僅将 C 檔案中将 C++ 中定義的 extern "C" 函數聲明為 extern 類型。   筆者編寫的 C 引用 C++ 函數例子工程中包含的三個檔案的源代碼如下: //C++ 頭檔案 cppExample.h #ifndef CPP_EXAMPLE_H #define CPP_EXAMPLE_H extern "C" int add( int x, int y ); #endif //C++ 實作檔案 cppExample.cpp #include "cppExample.h" int add( int x, int y ) {   return x + y; } extern int add( int x, int y ); int main( int argc, char* argv[] ) {   add( 2, 3 );   return 0; }   如果深入了解了第 3 節中所闡述的 extern "C" 在編譯和連接配接階段發揮的作用,就能真正了解本節所闡述的從 C++ 引用 C 函數和 C 引用 C++ 函數的慣用法。對第 4 節給出的示例代碼,需要特别留意各個細節。