在上一篇文章(C/C++编译模型分析)中介绍了C和C++中编译和链接的成因和方式。接上篇文章的坑,本篇文章从extern "C"
着手分析C和C++编译与链接模型中的不同点及其成因,主要为function overload
、function signatures
、name mangling
三个部分。
在介绍上面三个概念之前,先来简单了解一下C语言的编译模型,可参照我的上一篇文章:C/C++编译模型分析。
函数重载(function overload)
函数重载是一个十分有用的概念:我可以为同一个函数定义几种针对不同参数的不同实现。
[ISO/IEC 14882:014]**When two or more different declarations are specified for a single name in the same scope, that name is said to be **overloaded.
C语言中没有重载机制,这代表着C语言中不能有两个名字相同的函数。
即,下面的代码在C语言中具有重定义错误:
1 | int customMax(int x,int y){} |
所以可以在大量的C代码中看到类似于fs__xxx
这样的代码,以标识符名字命名的方式来指定其行为。
但是在C++中这是合法的,因为C++中具有函数重载(function overload)
。但是C++并没有什么关键字来表示一个函数具有重载(比如C#中的overload
关键字),这是因为C++之父Bjarne Stroustrup在设计C++之时,考虑了如果想要兼容已有C的C库的重载,在不破坏已有代码的基础上(非侵入式),做好对于重载关键字的处理是不容易的,所以C++使用了另外一种方式来实现函数重载。
C++的函数重载和链接
由于在C语言中没有重载,所以编译器对函数的链接也十分简单:函数的名字是customMax
,那么其在目标文件中的符号名字就是customMax
,我们可以来看一下:
对于函数:
1 | // customMax.c |
根据在上一篇文章中提到的生成目标代码的方法,来对其进行操作:
1 | # 生成customMax.c的目标文件customMax.o |
可以通过gcc工具链中的nm
来查看目标文件中的符号信息:
1 | # 查看customMax.o中的符号信息 |
函数符号类型:
- T 该符号放在当前目标文件的代码段中,通常是那些全局非static函数
- U 该符号在当前目标文件中未定义,需要从其他对象文件中链接进来
可以看到C语言中函数被编译器编译之后的符号仍然是函数名。但是在C++中这是个问题了,如果如同C语言这种简单的符号编译和链接方式,那么一个函数名怎么区分不同的重载版本?
1 | // error: conflicting types for 'customMax' |
在C中这两个函数编译出来的符号完全相同,从这一层来说,无法直接通过相同的符号名字(本身相同的符号名字都是具有歧义的)来区分一个不同的函数。
对于上面的问题,C++采取的方案的基本思想是:将类型信息编码传递到链接程序的名字里。
即对于函数的类型信息(返回类型,参数类型)编码到编译之后的符号名字中(以下仅为解释,非实际编译器行为):
1 | int customMax(int x,int y); |
来看一下编译器实际的行为。对于上面customMax
的代码来使用C++的编译器(g++
)来生成目标文件,且查看其中函数customMax
的符号信息:
1 | $ g++ -c customMax.c -o customMax.o |
此时我们往里面添加一个重载函数double customMax(double,double)
:
1 | // customMax.c |
再编译生目标文件,查看其中的符号信息:
1 | $ g++ -c customMax.c -o customMax.o |
可以看到,double custom(double,double)
与int custom(int,int)
在目标文件中的符号信息并不一样,这是支持C++重载的实现原则。
使用gcc工具链里的c++filt
可以将符号信息转换为原本的函数信息(至于为什么没有显示出函数的返回类型其原因是因为function Signatures
,暂时按下不表,后面会讲到):
1 | $ c++filt _Z3customMaxii |
可以看到同一个函数customMax
通过C语言编译器和C++编译器编译出来的符号信息是不同的,编译器对函数名字生成符号的操作叫做**名称改编(name mangling)**,暂时先按下不表,后文详述。
C++函数重载与C的兼容性
但是这么做会有一个问题:与C的兼容性。C++之父在设计C++时把兼容C语言当做头等大事,并要求C++与C语言之间不应该具有无故的不兼容。因为函数重载与C造成的不直接兼容是必须要接受的,但是C++提供了一种间接兼容的方式。
考虑下面的问题:
1 | // C code and using C compiler |
对于上面的函数customMax
,它可能位于某个静态链接库或动态链接库中——即非源码形式库。
这样我们不能对其进行侵入式的修改,现在有一个问题:因为C编译器没有对函数名字到符号之间进行修改(函数名就是目标文件中的符号名),所以上面的代码所产生的符号信息是customMax
,但是由于C++要兼容C的库,我想要在我的C++代码里调用这个链接库中的customMax
函数会产生什么样的效果呢?
即使用C的编译器编译函数customMax
的实现,用C++编译器编译对customMax
的调用。
1 | // customMax.c |
使用C编译器编译:
1 | # C编译器编译出customMax的目标文件customMax.o |
对于customMax
的调用代码:
1 | // main.c |
使用C++编译器编译:
1 | $ g++ -c main.c -o main.o |
来尝试将他们链接一下:
1 | $ g++ main.o customMax.o |
这是因为main.o中的符号名_Z6customMaxii
在链接时没有被找到,customMax.o中的符号是customMax
,他们并不匹配,所以连接错误。如下图:
extern "C"的引入
正是由于上面提到的原因,在C++的目标文件中不能直接连接到C库的目标文件中(这个目标文件包括.o以及使用C语言编译器生成的动静态链接库)。
这里C++调用C库的问题在于:如何把一个C++调用的函数“伪装成”C函数。为了实现这个,必须显式说明相关的函数具有C的链接,否则C++的编译器就会假定是C++函数,对其名字进行编码(name mangling)。
C++引入了一个链接描述符:
1 | extern "C"{ |
这种链接方式并不影响customMax
函数本来的语义,它只是告诉编译器,在目标代码里,对customMax
函数应该使用C编译器的符号命名规则。
就像前面的main.c中的代码,我们可以对其进行改写:
1 | // main.c |
这样我们在对其使用C++的编译器编译,并查看其customMax
函数的符号信息:
1 | $ g++ -c main.c -o main.o |
可以看到,customMax
的符号信息并不是_Z6customMaxii
了,那么,再使用先前使用C编译器编译的customMax.o
进行链接:
1 | # OK |
name mangling的规则
实际上,不同的编译器对于name mangling
的规则并不完全相同,关于C++ABI的相关内容可以看Itanium C++ ABI.
C++的函数重载是依赖与签名(signatures)
机制的。C++标准(本文引用标准为C++14)为不同类别的对象(包含函数)定义了一组签名规则(对象的链接符号皆依赖此规则(它指明了不同的对象具有什么样的性质,供编译器实现name mangling),利用签名进行函数匹配与重载解析不是本文要探寻的主要目标):
注:以下英文部分均引用自**[ISO/IEC 14882:2014]**
- function: name, parameter type list (8.3.5), and enclosing namespace (if any)
注意,函数的签名不包括返回类型,这也是前文中,c++filt转换符号名到函数名不会输出返回类型的原因。同时这也意味着,在C++中只有返回类型不同的函数并不是重载,而是重定义。
1 | // error: conflicting types for 'func' |
而且,函数的签名也不包括
CV-qualifiers
,这意味着重载的函数起参数类型如果只是具有CV-qualifiers
的区别,它也不是同在而是重定义。
1 | // error: conflicting types for 'func' |
- function template: name, parameter type list (8.3.5), enclosing namespace (if any), return type, and template parameter list.
- function template specialization: signature of the template of which it is a specialization and its template arguments (whether explicitly specified or deduced).
- class member function: name, parameter type list (8.3.5), class of which the function is a member, cv-qualifiers (if any), and ref-qualifier (if any).
- class member function template: name, parameter type list (8.3.5), class of which the function is a member,cv-qualifiers (if any), ref-qualifier (if any), return type, and template parameter list.
- class member function template specialization: signature of the member function template of which it is a specialization and its template arguments (whether explicitly specified or deduced)
C语言中调用C++函数
上面已经写道了C++中具有extern "C"
的原因以及nanme mangling
的规则,但是现在有一个问题:C语言可否调用由C++编译器编译的C++风格(name mangling)的函数呢?
先来说第一种,比较简单粗暴的办法:
直接指定目标文件中的符号名
1 | // customMax.cpp |
对其编译生成目标文件之后,可以查看其中的符号信息:
1 | $ g++ -c customMax.cpp -o customMax.o |
在得到该目标文件中customMax
的符号信息之后,可以在customMax.h
中写一个条件包含:
1 | // customMax.h |
就是当我们使用一个C++编译器来编译一个包含了customMax.h
的代码时就会被包含进来int customMax(int,int);
,而如果是使用非C++编译器编译时会包含int _Z9customMaxii(int,int);
。
使用再写一个调用customMax.o
中的customMax
的C代码(并且使用C语言编译器编译):
1 | // main.c |
对于上面的main.c
使用C语言编译器编译得到main.o
:
1 | $ gcc -c main.c -o main.o |
然后将与使用C++编译器编译的customMax.cpp
得到的目标文件customMax.o
链接起来:
1 | # 链接成功 |
但是这里有个非常麻烦的地方在于:要先获取到目标文件中的符号名再去修改包含的customMax.h
中的声明信息,非常繁琐,这种方式比较适用于在无法获取需要链接的库的源码的情况下,自己手动导出符号调用。
包装函数
如果我们可以获取到需要链接的库的源码,可以添加一个包装函数:使用extern "C"
包装一个目标函数(仍然使用customMax.cpp
举例):
1 |
|
这样在使用C++编译器编译该代码的时候会在目标文件中看到两个符号信息:
1 | $ g++ -c customMax.cpp -o customMax.o |
然后就可以在customMax.h
中写一个条件编译:
1 |
|
与上一种方式不同的是,我们不需要手动获取customMax.o
中的符号信息了,而是在库中导出一个特定的C语言接口。
然后同样使用main.c
来调用:
1 |
|
同样可以编译并且与使用C++编译器编译的customMax.cpp
链接成功。
注意:如果你在
customMax.cpp
中使用了C++的库,在链接时也必须把其的符号文件/静态/动态链接库指定进来,不然会出现未定义标识符错误。
因为C++编译器和C语言编译器在链接是使用的是不同的库(静/动态),在使用C++编译器并且链接是不会出现这样的问题,因为C++编译器会自动(隐式链接)从C++编译器的路径的库中扫描该符号信息。
例如,我在customMax.cpp
的customMax
函数中使用C++标准库:
1 |
|
对其使用C++编译器编译时不会报错,但是在与main.o
链接时会提示未定义标识符错误:
外部参考
更新日志
2017.04.12
- 增加代码示例
- 优化部分措辞,修改部分函数的命名
- 更新图片,使之对应上面的修改
2017.04.24
- 增加”C语言中调用C++函数”