导入
最近看公司项目源码,发现每个C头文件中都包含 EXTERN_STDC_BEGIN 和 EXTERN_STDC_END 这两个宏,如:
#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__
#include "msg_buffer.h"
EXTERN_STDC_BEGIN
void send(component *src, component *dest);
void receive(component *dest);
EXTERN_STDC_END
#endif
出于好奇,我查看了一下这两个宏的声明,发现:
#ifdef __cplusplus
#define EXTERN_STDC_BEGIN extern "C" {
#define EXTERN_STDC_END }
#else
#define EXTERN_STDC_BEGIN
#define EXTERN_STDC_END
#endif
这与我们平时经常看到的#ifdef __cplusplus extern “C” { #endif其实是一样的。
如果项目是纯C语言编写的,那 EXTERN_STDC_BEGIN 和 EXTERN_STDC_END 就是空宏,如上面代码段5、6行所示的那样。预处理过后,在使用#include命令包含了这个头文件的.c文件中,对应的#include语句就会被替换成如下形式:
#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__
#include "msg_buffer.h"
void send(component *src, component *dest);
void receive(component *dest);
#endif
而如果项目文件是由C和C++混合编程实现的,并且某些.cpp文件以#include的形式将这个头文件包含在内,那么在这些.cpp文件内部对应的#include语句就会被替换成如下形式:
#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__
#include "msg_buffer.h"
extern "C" {
void send(component *src, component *dest);
void receive(component *dest);
}
#endif
原理
那为什么需要有这个 extern “C” 呢?
这是因为在g++中有一种称为“名字粉碎”的机制,当对一个.cpp文件进行编译的时候,g++会将文件中的各个函数名进行“粉碎”,按照“_函数名_参数类型”的形式存储到目标文件符号表中。如:对于函数int add(int x, int y); 使用gcc编译过后,目标文件符号表中会生成类似_add的函数符号;而使用g++编译后,则会生成_add_int_int的函数符号,这也从一定程度上解释了C++中的函数重载机制。
此外,联想一下makefile中工程文件的编译链接原理:
编译器会将源代码文件(.c或.cpp文件)看做一个独立的编译单元生成目标文件,随后,链接器通过目标文件符号表将它们链接在一起得到一个最终的可执行文件。
编译和链接是两个不同阶段的事情,事实上,编译器和链接器是两个完全独立的工具。一般来说,
编译器可以通过语义分析知道那些同名的符号之间的差别,而链接器则只能通过目标文件符号表中保存的符号名来识别对象。
所以,g++编译器进行“名字粉碎”会将所有名字重新编码,生成全局唯一的新名字,让链接器能够准确识别每个名字所对应的对象,从而避免链接器在工作时陷入困惑。
然而 C语言是一种只有一个全局命名空间的语言,不允许进行函数重载。也就是说,在一个编译和链接范围之内,C语言不允许出现同名的函数或变量,因为C编译器不会对名字进行任何复杂的处理(或者仅仅对名字进行简单一致的修饰,如在名字前面统一加一个下划线_)。
C++的缔造者Bjarne Stroustrup在一开始就把能向下兼容C,即能够复用大量已经存在的C库作为C++的重要目标之一。然而,C和C++编译器对函数处理方式的不一致给链接的过程带来了一丁点的“麻烦”。、
举例
就拿上面那个头文件为例,此处我们假设文件名为communication.h,其实现放在对应的.c 源文件中。假定此时不使用extern “C”,如下所示:
#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__
#include "msg_buffer.h"
void send(component *src, component *dest);
void receive(component *dest);
#endif
我们使用gcc对其进行编译,生成目标文件communication.o。由于C编译器不会进行“名字粉碎”,因此在communication.o的符号表中,send和receive以_send和_receive的形式存放。
随着工程项目的进展,假设需要在另外一个.cpp文件调用这个头文件中声明的函数,因此,需要在这个.cpp文件中以#include的形式包含头文件communication.h。此处我们假定这个.cpp文件的名字为fcs.cpp,那么在编译时,C++编译器会进行“名字粉碎”,使得在目标文件fcs.o的符号表中会出现以下形式的函数符号名:_send_component_component和_receive_component。
要得到一个最终可执行的文件,还需要将communication.o和fcs.o放在一起进行链接。然而,由于在两个目标文件对于同一个函数的命名不一致,链接器将报告“符号未定义”的错误。
为了解决这一问题,C++引入了
链接规范
的概念,链接规范的作用是告诉C++编译器:
对所有使用链接规范进行修饰的声明或定义,应该按照其指定语言的方式进行处理。
链接规范的用法有两种:
1. 单个声明的链接规范,如:extern “C” void foo();
2. 一组声明的链接规范,如:
extern “C”
{
void foo();
int bar();
}
现在我们按照上面的方法将头文件communication.h修改成如下的形式:
#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__
#include "msg_buffer.h"
extern "C" {
void send(component *src, component *dest);
void receive(component *dest);
}
#endif
然后使用g++重新对fcs.cpp进行编译,所生成目标文件fcs.o的符号表中两个函数就会存储为_send和_receive的形式。这样,当再次把communication.o和fcs.o放在一起进行链接时,就不会出现“符号未定义”的错误了。
然而,此时如果重新发起整个工程的构建,编译器就会对communication.c重新进行编译,此时会报告“语法错误”,因为extern “C”是C++的语法,而communication.c文件是由gcc编译的。此时,可以按之前已经讨论的,使用__cplusplus对gcc和g++进行识别。修改后的communication.h的代码如下所示:
#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__
#include "msg_buffer.h"
#ifdef __cplusplus
extern "C" {
#endif
void send(component *src, component *dest);
void receive(component *dest);
#ifdef __cplusplus
}
#endif
#endif
这样,不论工程项目是否包含.cpp文件,编译过后,函数符号名都能保持统一的形式,链接时就不会出现问题了。
总结
在工程项目中,为了避免出现“符号未定义”等问题,头文件都以下面的形式进行编写:
#ifdef __cplusplus
extern "C" {
#endif
/* 函数声明 */
#ifdef __cplusplus
}
#endif
如果觉着在每个头文件里都把这6行写一次比较麻烦,我们可以定义两个宏,就像本文一开始的EXTERN_STDC_BEGIN和EXTERN_STDC_END宏那样,此时头文件就按以下形式编写:
EXTERN_STDC_BEGIN
/* 函数声明 */
EXTERN_STDC_END
这样看起来是不是就清爽很多了~