extern “C” 在C/C++中的使用
1 : 問題定義
在研究作業系統原始碼或者在嵌入式系統中編寫程式時,經常會發現下面這種用法:
[cpp] view plain copy
- #ifndef __OTHER_FILE_C_H__--------------------防止一個檔案中多次包含這個標頭檔案
- #define __OTHER_FILE_C_H__--------------------防止一個檔案中多次包含這個標頭檔案
- #ifdef __cplusplus------------------------如果使用的是C++編譯器
- extern "C" {
- #endif
- ……
- extern void c_main();-----這部分內容一般是函式宣告或一些資料結構的定義等
- ……
- #ifdef __cplusplus------------------------如果使用的是C++編譯器
- }
- #endif
- #endif
其實extern “C”可以用在函式定義之前,也可以用在函式宣告之前。這兩者的區別在後續內容中將會講到,但是一般用在函式宣告之前。
或許大家都知道,extern “C”的作用就是在C++環境中使函式按照C的標準來編譯和連結,但這種說話不全面。比如說當extern “C”放在函式宣告之前,就不會改變函式的編譯方式,只是指定編譯器按照C的標準連結,而不是按照C++的標準去連結函式。
其實在標頭檔案.h中下面這種用法
extern “C” externvoid c_main();
等效與在 .cpp檔案中直接用extern “C” void c_main();
但是有四個問題值得我們仔細思考:
1、 在C++編譯環境(VC 6.0)中,工程檔案中同時包含.c檔案和.cpp檔案,那麼編譯.c和.cpp按照什麼編譯規則來編譯的,有什麼區別?
2、 在C++編譯環境中, extern “C”放在函式宣告之前的作用是什麼?
3、 加了cpp檔案中在函式宣告前加extern “C”的作用是什麼?
4、 那麼什麼情況下才需要使用extern “C”呢?
除非有特別指出特定的編譯器,否則以下采用的實驗環境預設是VC 6.0。
2 :原理
C++語言的建立初衷是“a better C”,但是這並不意味著C++中類似C語言的全域性變數和函式所採用的編譯和連線方式與c語言完全相同。作為一種欲與C相容的語言,C++保留了一部分過程式語言的特點(被世人稱為“不徹底地面向物件”),因而它可以定義不屬於任何類的全域性變數和函式。但是,C++畢竟是一種面向物件的程式設計語言,為了支援函式的過載,C++對全域性函式的處理方式與C有明顯的不同。
extern "C" 包含雙重含義,從字面上即可得到:首先,被它修飾的目標是“extern”的;其次,被它修飾的目標是“C”的。讓我們來詳細解讀這兩重含義。
1. 被extern"C"限定的函式或變數是extern型別的;
2. 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 c_main();
該函式被C編譯器編譯後在符號庫中的名字為_ c_main,而C++編譯器則會產生像
[email protected]@YAXXZ之類的名字(不同的編譯器可能生成的名字不同,但是都採用了相同的機制)。
3:分析
3.1:VC中.c與.cpp的編譯規則
問題:在C++編譯環境(VC 6.0)中,工程檔案中同時包含.c檔案和.cpp檔案,那麼編譯.c和.cpp按照什麼編譯規則來編譯的,有什麼區別?
解答:在VC 6.0工程檔案中,可能含有.cpp檔案和.c檔案混合編譯。在.c的原始檔按照C的標準來編譯,函式名之前加入”_”,並把“_函式名”放入到符號表中,以後連結時使用。.cpp的原始檔按照C++的標準編譯,會修改函式名,這個不同的編譯器有不同的實現方式,在VC中,函式名被修改的有些亂,如下:
testC.c檔案中的程式碼:
[cpp] view plain copy
- #include "stdio.h"
- void c_main()
- {
- printf ("this isc program\n!");
- }
通過objdump工具反編譯testC.obj檔案為彙編程式碼為:
[html] view plain copy
- 00000000 <_c_main>:
- 0: 55 push %ebp
- 1: 8b ec mov %esp,%ebp
- 3: 83 ec 40 sub $0x40,%esp
- 6: 53 push %ebx
- 7: 56 push %esi
- 8: 57 push %edi
- 9: 8d 7d c0 lea -0x40(%ebp),%edi
- c: b9 10 00 00 00 mov $0x10,%ecx
- 11: b8 cc cc cc cc mov $0xcccccccc,%eax
- 16: f3 ab rep stos %eax,%es:(%edi)
- 18: 68 00 00 00 00 push $0x0
- 1d: e8 00 00 00 00 call 22 <_c_main+0x22>
- 22: 83 c4 04 add $0x4,%esp
有上述彙編程式碼可見,c_main函式編譯後,函式名為_c_main(), 可見.c檔案在VC中採用的是標準C的編譯方式。
testCPP.cpp檔案中的程式碼
[html] view plain copy
- 00000000 <[email protected]@[email protected]>:
- 0: 55 push %ebp
- 1: 8b ec mov %esp,%ebp
- 3: 83 ec 40 sub $0x40,%esp
- 6: 53 push %ebx
- 7: 56 push %esi
- 8: 57 push %edi
- 9: 8d 7d c0 lea -0x40(%ebp),%edi
- c: b9 10 00 00 00 mov $0x10,%ecx
- 11: b8 cc cc cc cc mov $0xcccccccc,%eax
- 16: f3 ab rep stos %eax,%es:(%edi)
- 18: 68 00 00 00 00 push $0x0
- 1d: e8 00 00 00 00 call 22 <[email protected]@[email protected]+0x22>
- 22: 83 c4 04 add $0x4,%esp
上述彙編程式碼可見,c_main函式編譯後,函式名為[email protected]@[email protected], 可見.cpp檔案在VC中採用的是標準C++的編譯方式。
3.2:extern “C”放在函式宣告前的作用
問題:在C++編譯環境中,用了(extern “C”+函式宣告)與(不用extern “C”+函式宣告),區別在哪?
解答:用(extern “C”+函式宣告),表明CPP此檔案中的函式,以C的標準來連結(“_函式名”), 如果在CPP檔案中函式宣告前不用extern “C”,則採用C++的標準來連結(比如函式名為”[email protected]@[email protected]”)。
Ø 如果在test_a.cpp檔案,有extern “C”放在函式宣告之前,而其函式實現放在另外一個檔案test_b.cpp中,則C++編譯器不會改變test_b.cpp檔案中函式的編譯規則(還是按照c++的規則來編譯),只是通知編譯器在連結test_a.cpp中函式時採取C的標準方式連結函式。如:vc 6.0工程中有兩個檔案,一個為test_a.cpp, 另一個檔案為test_b.cpp。 test_a.cpp檔案中程式碼如下:
[cpp] view plain copy
- #include "stdio.h"
- extern "C" void c_main2(int a, int b, int c);
- void main(void)
- {
- c_main2(0,0,0);
- return ;
- }
- test_b.cpp檔案中的程式碼如下:
- #include <stdio.h>
- void c_main2(int a, int b, int c)
- {
- printf ("this iscpp program\n!");
- }
結果:編譯通過,連結失敗。
1、 通過objdump工具檢視test_a.obj檔案的符號表,發現連結時需要查詢_c_main2()。
如下:
[html] view plain copy
- [ 14](sec 3)(fl0x00)(ty 20)(scl 2) (nx 1) 0x00000000 _main
- AUX tagndx 18 ttlsiz 0x37 lnnos 1224 next 0
- _main :
- 1 : 00000018
- 3 : 00000026
- [ 16](sec 0)(fl0x00)(ty 20)(scl 2) (nx 0) 0x00000000 _c_main2
- [ 17](sec 0)(fl0x00)(ty 20)(scl 2) (nx 0) 0x00000000 __chkesp
- [ 18](sec 3)(fl 0x00)(ty 0)(scl 101) (nx 1) 0x00000000 .bf
- 2、通過objdump工具反彙編test_b.obj,發現在c_main2()函式反彙編出來後,採用的是C++的編譯方式進行的,函式名做了更改[email protected]@[email protected],如下:
- 00000000 <[email protected]@[email protected]>:
- 0: 55 push %ebp
- 1: 8b ec mov %esp,%ebp
- 3: 83 ec 40 sub $0x40,%esp
- 6: 53 push %ebx
- 7: 56 push %esi
- 8: 57 push %edi
- 9: 8d 7d c0 lea -0x40(%ebp),%edi
- c: b9 10 00 00 00 mov $0x10,%ecx
- 11: b8 cc cc cc cc mov $0xcccccccc,%eax
- 16: f3 ab rep stos %eax,%es:(%edi)
- 18: 68 00 00 00 00 push $0x0
- 1d: e8 00 00 00 00 call 22 <[email protected]@[email protected]+0x22>
- 22: 83 c4 04 add $0x4,%esp
- 由於連結時在其他模組中找不到_c_main2()函式,故提示連結失敗。
3.3:extern “C”放在函式定義前的作用
問題:在cpp檔案中在函式定義前加了extern “C”後,此的函式定義的編譯方式是否會改變(按照c的編譯方式編譯還是按照c++的編譯方式編譯)?
Ø 解答: 會改變,如果extern “C”放在函式定義之前,則C++編譯器使得函式按照C的標準來編譯和連結函式。
如VC6.0工程中有以檔案test.cpp,檔案中程式碼內容如下:
[cpp] view plain copy
- #include"stdio.h"
- extern "C"void c_main2(int a, int b, int c)
- {
- printf ("this is cppprogram\n!");
- }
- void main(void)
- {
- c_main2(0,0,0);
- return ;
- }
結果:編譯連結都通過。
通過objdump工具檢視其生成的.obj檔案,發現生成的c_main2()函式的彙編程式碼為:
[html] view plain copy
- 00000000<_c_main2>:
- 0: 55 push %ebp
- 1: 8b ec mov %esp,%ebp
- 3: 83 ec 40 sub $0x40,%es
- 6: 53 push %ebx
- 7: 56 push %esi
- 8: 57 push %edi
- 9: 8d 7d c0 lea -0x40(%eb
- c: b9 10 00 00 00 mov $0x10,%ec
- 11: b8 cc cc cc cc mov $0xcccccc
- 16: f3 ab rep stos%eax,%e
- 18: 6800 00 00 00 push $0x0
- 1d: e8 00 00 00 00 call 22 <_c_ma
- 22: 83 c4 04 add $0x4,%esp
雖然c_main2()函式在CPP檔案中,但是在函式定義前加了extern “C”後,採用的是C標準編譯方式,在函式名前加下劃線,變成_c_main2()。該用GCC編譯,結果一樣,也使在函式名前加下劃線。
3.4:使用場景
問題:那麼什麼情況下才需要使用extern “C”呢?
解答:
1、 由於系統核心一般是使用C語言來編寫的,系統核心中用C語言實現了很多庫。而上層應用程式有可能是用C++來開發,如果在核心庫函式標頭檔案中不用extern “C”來宣告庫函式的話,在編寫C++應用程式時,包含庫標頭檔案,在C++檔案連結時就會以C++標準來連結庫的函式名,而在庫檔案實現時是用C來實現的,二者函式名不同,在連結時就會出現找不到函式的現象。
2、 在有些工程中,即包含.c檔案有包含.cpp檔案,如VC的工程。.cpp檔案要呼叫.c檔案中的函式時,需要extern “C” 宣告.c檔案中的函式,在CPP檔案中讓C++編譯器使用C的標準來連結C檔案中的函式
3.5:常用做法
在編寫C程式碼的同時,為了可以被C++程式呼叫,通常會在C原始碼對應的標頭檔案中加入
[cpp] view plain copy
- #ifdef __cplusplus------------------------如果使用的是C++編譯器
- extern "C" {
- #endif
- ……
- ……
- extern void c_main();------------------------------------外部函式介面宣告
- #ifdef __cplusplus------------------------如果使用的是C++編譯器
- }
- #endif
這樣做節省了維護程式碼的開銷,在CPP程式碼中需要呼叫C中的介面時,直接包含C的標頭檔案即可
當extern “C”的場合是當C程式呼叫C++的東西時:
按照如下步驟做即可
1. 在C++的.h檔案中用extern “C”{}宣告將被C程式使用的函式
2. 在C++的.cpp檔案中實現上面的函式
3. 在.c檔案中用extern宣告要使用的C++函式
4. 使用即可
注意:切不可在.c檔案中包含C++的.h檔案,那樣編譯無法通過
上程式碼:
CPPClass.h中宣告add函式
[cpp] view plain copy
- #ifndef __CPPClass_H__
- #define __CPPClass_H__
- extern "C"
- {
- int add(int a, int b) ;
- };
- #endif // end __CPPClass_H__
CPPClass.cpp實現add函式
[cpp] view plain copy
- #include "CPPClass.h"
- int add(int a, int b)
- {
- return a + b ;
- }
- main.c 內容如下
- #include <stdio.h>
- //#include "CPPClass.h" // 不要包含標頭檔案,否則編譯不過
- extern int add(int a, int b) ; // 只需顯示宣告要呼叫的函式即可
- int main(void)
- {
- int result = add(1, 2) ; //使用函式
- printf("%d", result) ;
- return 0 ;
- }