很多人都知道,在C++中调用C的api, 需要在C++中使用extern "C"来修饰。那么,为什么呢???
我们需要先来了解几个概念:
符号: 在链接中,我们将函数和变量统称为符号(Symbol), 函数名或变量名就是符号名(Symbol Name)。
每个目标文件(使用gcc -c参数编译出来的.o文件)都会有一个相应的符号表(Symbol Table)。这个表里面记录了目标文件中所用到的所有符号。每个定义的符号都有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是他们的地址。
首先我们来了解下符号表。我们都知道gcc编译出来的可执行文件elf格式中有个段: text代码段、data数据段保存已初始化的全局变量、bss段保存未初始化的变量等等。其实还有其他段,我们可以用readelf命令来看下:
我们先写个简单的代码:
/* file : lib.h */
#ifndef __LIB_H__
#define __LIB_H__
void foobar();
#endif
#endif
/* file: lib.c */
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "lib.h"
int number = 0;
void foobar()
{
printf("PID:%d number: %d\n",getpid(),number);
number++;
printf("PID:%d number: %d\n",getpid(),number);
}
编译:
下面我们可以使用readelf -S来查看目标文件了。
好了,现在我们知道符号表里存着我们在代码里一些变量名和函数,我们还可以使用nm命令来查看具体的符号值。
接着,同学们可能会有疑问,讲了那么多符号的内容,可是跟我extern C有什么关系???逗我呢???
其实我们很接近答案了,我们都知道C++使用g++来编译,那我们就编译一下看看里面的符号表好了。
有没有发现?使用gcc或者g++编译的,生成的目标文件的符号不一样!!!
假如我们给 foobar 加上 extern "C"修饰:
#ifndef __LIB_H__
#define __LIB_H__
#ifdef __cplusplus
extern "C" {
#endif
void foobar();
#ifdef __cplusplus
}
#endif
#endif
编译结果:
又变回我们熟悉的foobar了。需要特别注意的是,在g++编译时,会自动定义__cplusplus, 所以可以根据这个宏定义来判断是编译C还是C++.
其实上述内容涉及到符号修饰与函数签名。我们都知道在c++中拥有类、继承、重载、名称空间等特性,这些特性使得符号管理非常复杂。比如:两个名字相同但参数不同的函数foo(int )和foo(double ),按照C的特性是无法处理,会报编译错误信息。但在C++中,这两个函数签名并不相同,所以并不冲突。注意,函数签名不仅跟函数名有关系,还跟参数有关系,所以函数签名不一样。
在C++中,为了支持这些复杂的特性,人们发明了符号修饰(Name Decroation)的机制
例如在上述代码中,
void foobar()函数在C++中经过符号修饰的后的名称变为_Z6foobarv
具体符号修饰的规则就不在本节介绍了。我们要了解清楚extern C的作用就好了。extern C就是告诉编译器不要使用c++的符号修饰,这样生成的符号跟C还是一样的,就能在C++中链接C库了。