1. 程式人生 > >extern 和 external“C”的分析

extern 和 external“C”的分析

extern 和 extern "C" 分析
extern
extern是C/C++語言中表明函式和全域性變數作用範圍(可見性)的關鍵字:它告訴編譯器,其宣告的函式和變數可以在本模組或其它模組中使用。
1。對於extern變數來說,僅僅是一個變數的宣告,其並不是在定義分配記憶體空間。如果該變數定義多次,會有連線錯誤
2。通常,在模組的標頭檔案中對本模組提供給其它模組引用的函式和全域性變數以關鍵字 extern宣告。也就是說c檔案裡面定義,如果該函式或者變數與開放給外面,則在h檔案中用extern加以宣告。所以外部檔案只用include該h 檔案就可以了。而且編譯階段,外面是找不到該函式的,但是不報錯。link階段會從定義模組生成的目的碼中找到此函式。
3。與extern對應的關鍵字是static,被它修飾的全域性變數和函式只能在本模組中使用。

1 問題:常常見extern放在函式的前面成為函式宣告的一部分,那麼,C語言的關鍵字extern在函式的宣告中起什麼作用? 

答案與分析:如果函式的宣告中帶有關鍵字extern,僅僅是暗示這個函式可能在別的原始檔裡定義,沒有其它作用。即下述兩個函式宣告沒有明顯的區別:extern int fun() 和 int fun();當然,這樣的用處還是有的,就是在程式中取代include “*.h”來宣告函式,在一些複雜的專案中,我比較習慣在所有的函式宣告前新增extern修飾。

2 問題:extern 變數在一個原始檔裡定義了一個數組: char a[6]; 在另外一個檔案裡用下列語句進行了宣告: extern char *a;  請問,這樣可以嗎?

 答案與分析:1)、不可以,程式執行時會告訴你非法訪問。原因在於,指向型別T的指標並不等價於型別T的陣列。extern char *a宣告的是一個指標變數而不是字元陣列,因此與實際的定義不同,從而造成執行時非法訪問。應該將宣告改為extern char a[ ]。  2)、例子分析如下,如果a[] = "abcd",則外部變數a=0x61626364 (abcd的ASCII碼值),*a顯然沒有意義,如下圖:  顯然a指向的空間(0x61626364)沒有意義,易出現非法記憶體訪問。  3)、這提示我們,在使用extern時候要嚴格對應宣告時的格式,在實際程式設計中,這樣的錯誤屢見不鮮。  4)、extern用在變數宣告中常常有這樣一個作用,你在*.c檔案中聲明瞭一個全域性的變數,這個全域性的變數如果要被引用,就放在*.h中並用extern來宣告。 

3 問題:extern 函式   當函式提供方單方面修改函式原型時,如果使用方不知情繼續沿用原來的extern申明,這樣編譯時編譯器不會報錯。但是在執行過程中,因為少了或者多了輸入引數,往往會照成系統錯誤,這種情況應該如何解決? 

答案與分析:  目前業界針對這種情況的處理沒有一個很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供對外部介面的宣告,然後呼叫方include該標頭檔案,從而省去extern這一步。以避免這種錯誤。  

寶劍有雙鋒,對extern的應用,不同的場合應該選擇不同的做法。 extern "C" 某企業曾經給出如下的一道面試題:面試題為什麼標準標頭檔案都有類似以下的結構?

#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 的作用又是什麼呢?
extern "C" 包含雙重含義,從字面上即可得到:首先,被它修飾的目標是“extern”的;其次,被它修飾的目標是“C”的。
讓我們來詳細解讀這兩重含義。
(1)extern "C"限定的函式或變數是extern型別的; extern是C/C++語言中表明函式和全域性變數作用範圍(可見性)的關鍵字,該關鍵字告訴編譯器,其宣告的函式和變數可以在本模組或其它模組中使用。記住,下列語句: extern int a; 僅僅是一個變數的宣告,其並不是在定義變數a,並未為a分配記憶體空間。變數a在所有模組中作為一種全域性變數只能被定義一次,否則會出現連線錯誤。通常,在模組的標頭檔案中對本模組提供給其它模組引用的函式和全域性變數以關鍵字extern宣告。例如,如果模組B欲引用該模組A中定義的全域性變數和函式時只需包含模組A的標頭檔案即可。這樣,模組B中呼叫模組A中的函式時,在編譯階段,模組B雖然找不到該函式,但是並不會報錯;它會在連線階段中從模組 A編譯生成的目的碼中找到此函式。
與extern對應的關鍵字是static,被它修飾的全域性變數和函式只能在本模組中使用。因此,一個函式或變數只可能被本模組使用時,其不可能被extern “C”修飾。
(2)被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"
{ #i nclude "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
// 這樣會編譯出錯:#i nclude "cExample.h"
extern int add( int x, int y );
int main( int argc, char* argv[] )

add( 2, 3 ); 

return 0;

 }

簡短一點就是: 函式經過編譯系統的翻譯成彙編,函式名對應著彙編標號。因為C編譯函式名與得到的彙編代號基本一樣,如:fun()=>_fun, main=>_main 但是C++中函式名與得到的彙編代號有比較大的差別。如:由於函式過載,函式名一樣,但彙編代號絕對不能一樣。為了區分,編譯器會把函式名和引數型別合在一起作為彙編代號,這樣就解決了過載問題。具體如何把函式名和引數型別合在一起,要看編譯器的幫助說明了。這樣一來,如果C++呼叫C,如fun(),則呼叫名就不是C的翻譯結果_fun, 而是帶有引數資訊的一個名字,因此就不能呼叫到fun(),為了解決 這個問題,加上extern "C"表示該函式的呼叫規則是C的規則,則呼叫時就不使用C++規則的帶有引數資訊的名字,而是_fun,從而達到呼叫C函式的目的。
具體介紹
時常在cpp的程式碼之中看到這樣的程式碼:
#ifdef __cplusplus
extern "C" {
#endif
//一段程式碼
#ifdef __cplusplus
}
#endif 
  這樣的程式碼到底是什麼意思呢?首先,__cplusplus是cpp中的自定義巨集,那麼定義了這個巨集的話表示這是一段cpp的程式碼,也就是說,上面的程式碼的含義是:如果這是一段cpp的程式碼,那麼加入extern "C"{和}處理其中的程式碼。

  要明白為何使用extern "C",還得從cpp中對函式的過載處理開始說起。在c++中,為了支援過載機制,在編譯生成的彙編碼中,要對函式的名字進行一些處理,加入比如函式的返回型別等等.而在C中,只是簡單的函式名字而已,不會加入其他的資訊.也就是說:C++和C對產生的函式名字的處理是不一樣的.
  比如下面的一段簡單的函式,我們看看加入和不加入extern "C"產生的彙編程式碼都有哪些變化:
int f(void)
{
return 1;

 在加入extern "C"的時候產生的彙編程式碼是:
.file "test.cxx"
.text
.align 2
.globl _f
.def _f; .scl 2; .type 32; .endef
_f:
pushl %ebp
movl %esp, %ebp
movl $1, %eax
popl %ebp
ret 
  但是不加入了extern "C"之後
.file "test.cxx"
.text
.align 2
.globl __Z1fv
.def __Z1fv; .scl 2; .type 32; .endef
__Z1fv:
pushl %ebp
movl %esp, %ebp
movl $1, %eax
popl %ebp
ret 
  兩段彙編程式碼同樣都是使用gcc -S命令產生的,所有的地方都是一樣的,唯獨是產生的函式名,一個是_f,一個是__Z1fv。
  明白了加入與不加入extern "C"之後對函式名稱產生的影響,我們繼續我們的討論:為什麼需要使用extern "C"呢?C++之父在設計C++之時,考慮到當時已經存在了大量的C程式碼,為了支援原來的C程式碼和已經寫好C庫,需要在C++中儘可能的支援C,而extern "C"就是其中的一個策略。
  試想這樣的情況:一個庫檔案已經用C寫好了而且執行得很良好,這個時候我們需要使用這個庫檔案,但是我們需要使用C++來寫這個新的程式碼。如果這個程式碼使用的是C++的方式連結這個C庫檔案的話,那麼就會出現連結錯誤.我們來看一段程式碼:首先,我們使用C的處理方式來寫一個函式,也就是說假設這個函式當時是用C寫成的:
//f1.c
extern "C"
{
void f1()
{
return;
}

  編譯命令是:gcc -c f1.c -o f1.o 產生了一個叫f1.o的庫檔案。再寫一段程式碼呼叫這個f1函式:
// test.cxx
//這個extern表示f1函式在別的地方定義,這樣可以通過
//編譯,但是連結的時候還是需要
//連結上原來的庫檔案.
extern void f1();
int main()
{
f1();
return 0;

  通過gcc -c test.cxx -o test.o 產生一個叫test.o的檔案。然後,我們使用gcc test.o f1.o來連結兩個檔案,可是出錯了,錯誤的提示是:
test.o(.text + 0x1f):test.cxx: undefine reference to 'f1()' 
  也就是說,在編譯test.cxx的時候編譯器是使用C++的方式來處理f1()函式的,但是實際上鍊接的庫檔案卻是用C的方式來處理函式的,所以就會出現連結過不去的錯誤:因為連結器找不到函式。
  因此,為了在C++程式碼中呼叫用C寫成的庫檔案,就需要用extern "C"來告訴編譯器:這是一個用C寫成的庫檔案,請用C的方式來連結它們。
  比如,現在我們有了一個C庫檔案,它的標頭檔案是f.h,產生的lib檔案是f.lib,那麼我們如果要在C++中使用這個庫檔案,我們需要這樣寫:
extern "C"
{
#include "f.h"

  回到上面的問題,如果要改正連結錯誤,我們需要這樣子改寫test.cxx:
extern "C"
{
extern void f1();
}
int main()
{
f1();
return 0;

  重新編譯並且連結就可以過去了.

  總結
  C和C++對函式的處理方式是不同的.extern "C"是使C++能夠呼叫C寫作的庫檔案的一個手段,如果要對編譯器提示使用C的方式來處理函式的話,那麼就要使用extern "C"來說明。