1.引言
有一道经典的程序员面试题,如下:
//为什么标准头文件都有和以下 相似的结构?
#ifndef DuNiangHeadFile
#define DuNiangHeadFile
#ifdef __cplusplus
extern "C" {
#endif
/*bla bla bla*/
#ifdef __cplusplus
}
#endif
#endif /* DuNiangHeadFile */
对于头文件中的编译宏的作用,显然是为了防止该头文件被重复引用。
#ifndef DuNiangHeadFile
#define DuNiangHeadFile
#endif /* DuNiangHeadFile */
那么剩下代码的作用又是什么呢?
#ifdef __cplusplus
extern "C" {
#endif
/*bla bla bla*/
#ifdef __cplusplus
}
#endif
这正是本篇博文所要阐述的内容。下面我们进入正题。
2.揭密extern "C"
从直观上来讲,extern "C" 显然有两层含义。其一,是 被它修饰的目标是“extern”,即该目标具有外部链接性,可以在其他编译单元(文件)中被引用。其二,被它修饰的目标是“C”类型的,即编译器或链接器要按照C的编译规则来对其进行编译或链接。
之所以采用这种方式,是因为C和C++这两种语言之间的一些差异导致的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
int myFunc(int a, int b);
经过C++编译器编译之后 ,在其目标文件.o 文件中,有一个_Z10myFuncii 符号,这个符号就代表了源文件中的int myFunc(int a, int b)函数了。不同的编译器可能生成不同的名字,但是都采用了相同的机制,即C++ Primer中所说的“名字粉碎”(name mangling)或“名字修饰”(name decoration)。
_Z10myFuncii 这样的名字包含了函数名、函数参数数量及类型信息,其最后的两个字符i 就表示第一参数和第二参数都是整型。C++就是靠这种机制来实现函数重载的。在链接阶段,链接器就会从生成的 .o目标文件中寻找_Z10myFuncii 这样的符号,从而生成可执行文件。
而对于加extern "C"声明后,即假设某个函数的原型为:
#ifdef __cplusplus
extern "C" {
#endif
int myFunc(int a, int b);
#ifdef __cplusplus
}
#endif
编译生成myFunc函数的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;链接器在为其他的目标代码寻找myFunc函数调用时,寻找的是未经修改的符号名_myFunc。正是因为如此,才实现了C++与C及其它语言的混合编程。
3.extern "C"的惯用法
明白了C++中extern "C"的基本原理,下面我们来具体分析一下extern "C"的常用技巧。
(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
extern "C"
{
#include "cExample.h"
}
而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在C文件的头文件中直接extern "C"时会出现编译语法错误。但是,说的是直接使用会出错。如果略施小计,C语言头文件中也是可以使用extern "C"的。
用g++编译cpp程序时,编译器会定义宏 __cplusplus ,可根据__cplusplus是否已经定义,来决定是否需要extern "C"。
#ifdef __cpluscplus
extern "C" {
#endif
//正常C语言头文件
#ifdef __cplusplus
}
#endif
这是你可以修改C语言.h文件的情况。
(2)当你不能修改C语言.h文件,比如这个头文件是公司里的前辈写的,或者,公司规定禁止修改没有BUG的历史遗留代码,以防引入不必要的新Bug。
同时,.h文件中没有extern "C"关键字,而你的C++程序又要链接使用由C编译好的.o目标文件。这时该怎么办呢?
可以这样,在你的c++文件中,包含该模块的头文件时加上extern "C", 如下:
extern "C" {
#include "old_C_HeadFile.h"
}
如果仅仅使用其中的一个函数,而不需要include整个头文件时,也可以单独声明该函数,如下:
extern "C" {
int myFunc(int , int);
}
这样,C++编译器在编译,或链接器在链接的时候,就会以C风格在目标文件中生成或寻找目标符号了。
同理,如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。这突然让我想到了com组件技术。