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
在所有模塊中作為一種全局變量只能被定義一次,否則會出現連接錯誤。
通常,在模塊的頭文件中對本模塊提供給其它模塊引用的函數和全局變量以關鍵字
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
函數例子工程中包含的三個文件的源代碼如下:
/* c
語言頭文件:
cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y); //注:寫成extern "C" int add(int , int ); 也可以
#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" //注:此處不妥,如果這樣編譯通不過,換成 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;
}
/* 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
節給出的示例代碼,需要特別留意各個細節。