淺入深出深刻理解C中的extern意圖

extern能夠置於變量或者函數前,以表示變量或者函數的定義在別的文件中,提示編譯器遇到此變量和函數時在其餘模塊中尋找其定義。另外,extern也可用來進行連接指定。c++

extern 變量面試

  在一個源文件裏定義了一個數組:char a[6];編程

  在另一個文件裏用下列語句進行了聲明:extern char *a;數組

  請問,這樣能夠嗎?ide

  答案與分析:函數

  1)、不能夠,程序運行時會告訴你非法訪問。緣由在於,指向類型T的指針並不等價於類型T的數組。extern char *a聲明的是一個指針變量而不是字符數組,所以與實際的定義不一樣,從而形成運行時非法訪問。應該將聲明改成extern char a[ ]。設計

  2)、例子分析以下,若是a[] = "abcd",則外部變量a=0x12345678 (數組的起始地址),而*a是從新定義了一個指針變量a的地址多是0x87654321,直接使用*a是錯誤的.指針

  3)、這提示咱們,在使用extern時候要嚴格對應聲明時的格式,在實際編程中,這樣的錯誤家常便飯。對象

  4)、extern用在變量聲明中經常有這樣一個做用,你在*.c文件中聲明瞭一個全局的變量,這個全局的變量若是要被引用,就放在*.h中並用extern來聲明。接口

  這個關鍵字真的比較可惡,在聲明(函數)的時候,這個extern竟然能夠被省略,因此會讓你搞不清楚究竟是聲明仍是定義,下面分變量和函數兩類來講:

  尤爲是對於變量來講。

  extern int a;//聲明一個全局變量a

  int a; //定義一個全局變量a

  extern int a =0 ;//聲明一個全局變量a 並給初值。

  int a =0;//定義一個全局變量a,並給初值,

  聲明以後你不能直接使用這個變量,須要定義以後才能使用。

  第四個等於第三個,都是定義一個能夠被外部使用的全局變量,並給初值。

  糊塗了吧,他們看上去可真像。可是定義只能出如今一處。也就是說,無論是int a;仍是extern int a=0;仍是int a=0;都只能出現一次,而那個extern int a能夠出現不少次。

  當你要引用一個全局變量的時候,你就要聲明extern int a;這時候extern不能省略,由於省略了,就變成int a;這是一個定義,不是聲明。

編譯、連接

一、 聲明外部變量

  現代編譯器通常採用按文件編譯的方式,所以在編譯時,各個文件中定義的全局變量是

  互相不透明的,也就是說,在編譯時,全局變量的可見域限制在文件內部。下面舉一個簡單的例子。建立一個工程,裏面含有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;

  順利經過編譯,連接。

 

函數

  extern 函數1

  常見extern放在函數的前面成爲函數聲明的一部分,那麼,C語言的關鍵字extern在函數的聲明中起什麼做用?

  答案與分析:

  若是函數的聲明中帶有關鍵字extern,僅僅是暗示這個函數可能在別的源文件裏定義,沒有其它做用。即下述兩個函數聲明沒有明顯的區別:

  extern int f(); 和int f();

  固然,這樣的用處仍是有的,就是在程序中取代include 「*.h」來聲明函數,在一些複雜的項目中,我比較習慣在全部的函數聲明前添加extern修飾。

  extern 函數2

  當函數提供方單方面修改函數原型時,若是使用方不知情繼續沿用原來的extern申明,這樣編譯時編譯器不會報錯。可是在運行過程當中,由於少了或者多了輸入參數,每每會形成系統錯誤,這種狀況應該如何解決?

  答案與分析:

  目前業界針對這種狀況的處理沒有一個很完美的方案,一般的作法是提供方在本身的xxx_pub.h中提供對外部接口的聲明,而後調用包涵該文件的頭文件,從而省去extern這一步。以免這種錯誤。

  寶劍有雙鋒,對extern的應用,不一樣的場合應該選擇不一樣的作法。

  extern 「C」

  在C++環境下使用C函數的時候,經常會出現編譯器沒法找到obj模塊中的C函數定義,從而致使連接失敗的狀況,應該如何解決這種狀況呢?

  答案與分析:

  C++語言在編譯的時候爲了解決函數的多態問題,會將函數名和參數聯合起來生成一箇中間的函數名稱,而C語言則不會,所以會形成連接時找不到對應函數的狀況,此時C函數就須要用extern 「C」進行連接指定,這告訴編譯器,請保持個人名稱,不要給我生成用於連接的中間函數名。

  下面是一個標準的寫法:

  //在.h文件的頭上

  #ifdef __cplusplus

  #if __cplusplus

  extern "C"{

  #endif

  #endif

  …

  …

  //.h文件結束的地方

  #ifdef __cplusplus

  #if __cplusplus

  }

  #endif

  #endif

  C++中extern c的深層探索

  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在全部模塊中做爲一種全局變量只能被定義一次,不然會出現鏈接錯誤。

  引用一個定義在其它模塊的全局變量或函數(如,全局函數或變量定義在A模塊,B欲引用)有兩種方法,1、B模塊中include模塊A的頭文件。2、模塊B中對欲引用的模塊A的變量或函數從新聲明一遍,並前加extern關鍵字。

  一般,在模塊的頭文件中對本模塊提供給其它模塊引用的函數和全局變量以關鍵字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);

  #endif

  

  #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;

  }

  

  extern int add( int x, int y );

  int main( int argc, char* argv[] )

  {

  add( 2, 3 );

  return 0;

  }

相關文章
相關標籤/搜索