C++中extern “C”含義及extern、static關鍵字淺析


https://blog.csdn.net/bzhxuexi/article/details/31782445 

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 /* __INCvxWorksh */


  分析
  顯然,頭文件中的編譯宏“#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在所有模塊中作為一種全局變量只能被定義一次,否則會出現連接錯誤。

  在C語言中,修飾符extern用在變量或者函數的聲明前,用來說明“此變量/函數是在別處定義的,要在此處引用”。

  與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函數例子工程中包含的三個文件的源代碼如下:

/* c語言頭文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c語言實現文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++實現文件,調用add:cppFile.cpp
extern "C" 
{
#include "cExample.h"
}
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;
}
/* C實現文件 cFile.c
/* 這樣會編譯出錯:#include "cExample.h" */
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節給出的示例代碼,需要特別留意各個細節。 

========================================================

extern "C"---------跨平台

 

在上世紀70年代以前,編譯器編譯源代碼產生目標文件時,符號名與相應的變量及函數的名稱是一樣的。比如一個代碼里面包涵了一個foo函數,那么編譯成目標文件以后,其中對應的符號名也是foo。當后來UNIX平台和C語言發明時,已經存在了相當多這樣的庫和目標文件。這就產生了一個問題,如果一個C程序要使用這些庫的話,它就不可以使用這些庫中定義的函數和變量做為符號名,否則就會和現有的目標文件沖突。比如有個用匯編編寫的庫中定義了一個函數或變量叫做main,那其他所有調用這個庫的代碼里就不能再定義main了。

         為了防止類似的符號名沖突,UNIX下的C語言就規定,C語言源代碼文件中所有的全局變量和函數經過編譯后,要在相應的符號名前加上下划線"_",而Fortran則要求在所有符號名前加上"_",后面也加上"_"。比如foo函數,C語言編譯后的符號名是"_foo",而Fortran的則是"_for_"。這種簡單而原始的方法的確能暫時減少多種語言的目標文件之間符號沖突的概率,但並沒有從根本上解決此問題。比如同一種語言編寫的目標文件還是有可能會產生符號沖突,當程序很大尤其是由不同部門或個人開發時,也有可能導致沖突。當然,隨着時間的推移,很多操作系統和編譯器被完全重寫了好幾遍。UNIX也分化成了很多種,整個環境也發生了很大的變化,上面提到的C語言與Fortran、匯編庫的符號沖突問題也已經不是那么明顯了。現在的Linux GCC編譯器,默認情況下已經去掉了在C語言符號名前加"_"的方式,在Windows平台下的編譯器還保持着這樣的傳統。

         C++這樣的后來設計的語言開始考慮到這個問題,增加了名稱空間(Namespace)的方法來解決多模塊的符號沖突問題,但是強大而復雜的C++擁有的類、繼承、虛機制、重載等特性無疑又使符號管理更為復雜。舉個簡單的例子,兩個相同名稱的函數func(int)和func(double),函數名相同但參數列表不通,這是C++里函數重載的最簡單的一種情況,那么編譯器和鏈接器在鏈接過程中如何區分這兩個函數呢?下面我們引入一個術語叫函數簽名,它包含了一個函數的信息:函數名、參數類型、所在的類和名稱空間及其他信息。函數簽名用於識別不同的函數,而函數的名稱只是函數簽名的一部分。由於上例中兩個同名函數的參數類型不同,我們可以認為它們的函數簽名不同而認為是不同的函數。在編譯器及鏈接器處理符號時,它們則使用某種名稱修飾的方法,使得每個函數簽名對應一個修飾后名稱。編譯器在將C++源代碼編譯成目標文件時,會將函數和變量的名稱進行修飾,形成符號名,目標文件中所使用的符號名就是修飾后名稱,所以對於不同函數簽名的函數,即使函數名相同,編譯器和鏈接器都認為它們是不同的函數。

如下面6個函數的函數簽名在GCC編譯器下獲得的修飾后名稱如下:

         簽名和名稱修飾機制不光被使用在函數上,C++中的全局變量和靜態變量也有同樣的機制。不同的編譯器廠商的名稱修飾方法可能不同,這里就不詳細描述其細節及異同了。

        相信說到這里,大家都明白為什么在跨平台的C++代碼中經常可以見到extern "C"的寫法了吧?顯然C++編譯器會將在extern "C"大括號內部的代碼當做C語言代碼處理,C++的名稱修飾機制不會對此起作用。上文說到對於一個函數foo,Linux版本的GCC不會將foo函數修飾成_foo,而Visual C++卻會將C語言代碼修飾成_foo。而在很多時候,我們會碰到有些頭文件聲明了一些C語言的函數和全局變量,但這個頭文件可能會被C或C++代碼包含。比如C語言庫函數string.h中的memset函數,原型如下:

         void*memset(void *, int , size_t);

如果不加任何處理,當C語言程序包含string.h並用到memset函數,編譯器能正常處理memset符號;但在C++語言中,編譯器會將memset函數簽名修飾成_Z6memsetPvii,這樣鏈接器就無法和C語言庫中的memset符號進行鏈接了。所以對於C++來說,必須使用extern "C"來聲明此函數,針對C和C++代碼的不同,編譯器使用宏"__cplusplus"來區分。


========================================================

                                   extern、static關鍵字淺析

 

static是C++中常用的修飾符,它被用來控制變量的存貯方式和可見性。extern, "C"是使C++能夠調用C寫作的庫文件的一個手段,如果要對編譯器提示使用C的方式來處理函數的話,那么就要使用extern "C"來說明。

一.C語言中的static關鍵字

在C語言中,static可以用來修飾局部變量,全局變量以及函數。在不同的情況下static的作用不盡相同。

(1)修飾局部變量

一般情況下,對於局部變量是存放在棧區的,並且局部變量的生命周期在該語句塊執行結束時便結束了。但是如果用static進行修飾的話,該變量便存放在靜態數據區,其生命周期一直持續到整個程序執行結束。但是在這里要注意的是,雖然用static對局部變量進行修飾過后,其生命周期以及存儲空間發生了變化,但是其作用域並沒有改變,其仍然是一個局部變量,作用域僅限於該語句塊。

在用static修飾局部變量后,該變量只在初次運行時進行初始化工作,且只進行一次。

如:

 
          
  1. #include<stdio.h>  
  2. void fun()  
  3. {   
  4. static int a=1; a++;   
  5. printf("%d\n",a);  
  6. }  
  7. int main(void)  
  8. {   
  9. fun();   
  10. fun();   
  11. return 0;  
  12. }  

程序執行結果為: 2  3

說明在第二次調用fun()函數時,a的值為2,並且沒有進行初始化賦值,直接進行自增運算,所以得到的結果為3.

對於靜態局部變量如果沒有進行初始化的話,對於整形變量系統會自動對其賦值為0,對於字符數組,會自動賦值為'\0'.

(2)修飾全局變量

對於一個全局變量,它既可以在本源文件中被訪問到,也可以在同一個工程的其它源文件中被訪問(只需用extern進行聲明即可)。

如:

 
          
  1. //有file1.c  
  2. int a=1;  
  3. file2.c  
  4. #include<stdio.h>  
  5. extern int a;  
  6. int main(void)  
  7. {  
  8. printf("%d\",a);  
  9. return 0;  

 

則執行結果為 1

但是如果在file1.c中把int a=1改為static int a=1;

那么在file2.c是無法訪問到變量a的。原因在於用static對全局變量進行修飾改變了其作用域的范圍,由原來的整個工程可見變為本源文件可見。

(3)修飾函數

用static修飾函數的話,情況與修飾全局變量大同小異,就是改變了函數的作用域。

二.C++中的static

在C++中static還具有其它功能,如果在C++中對類中的某個函數用static進行修飾,則表示該函數屬於一個類而不是屬於此類的任何特定對象;如果對類中的某個變量進行static修飾,表示該變量為類以及其所有的對象所有。它們在存儲空間中都只存在一個副本。可以通過類和對象去調用。

三.extern關鍵字

在C語言中,修飾符extern用在變量或者函數的聲明前,用來說明“此變量/函數是在別處定義的,要在此處引用”。
在上面的例子中可以看出,在file2中如果想調用file1中的變量a,只須用extern進行聲明即可調用a,這就是extern的作用。在這里要注意extern聲明的位置對其作用域也有關系,如果是在main函數中進行聲明的,則只能在main函數中調用,在其它函數中不能調用。其實要調用其它文件中的函數和變量,只需把該文件用#include包含進來即可,為啥要用extern?因為用extern會加速程序的編譯過程,這樣能節省時間。

在C++中extern還有另外一種作用,用於指示C或者C++函數的調用規范。比如在C++中調用C庫函數,就需要在C++程序中用extern “C”聲明要引用的函數。這是給鏈接器用的,告訴鏈接器在鏈接的時候用C函數規范來鏈接。主要原因是C++和C程序編譯完成后在目標代碼中命名規則不同,用此來解決名字匹配的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM