为什么需要extern "C"?

在上一篇文章(C/C++编译模型分析)中介绍了C和C++中编译和链接的成因和方式。接上篇文章的坑,本篇文章从extern "C"着手分析C和C++编译与链接模型中的不同点及其成因,主要为function overloadfunction signaturesname 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
2
int customMax(int x,int y){}
double customMax(double l,double r){}

所以可以在大量的C代码中看到类似于fs__xxx这样的代码,以标识符名字命名的方式来指定其行为。
但是在C++中这是合法的,因为C++中具有函数重载(function overload)。但是C++并没有什么关键字来表示一个函数具有重载(比如C#中的overload关键字),这是因为C++之父Bjarne Stroustrup在设计C++之时,考虑了如果想要兼容已有C的C库的重载,在不破坏已有代码的基础上(非侵入式),做好对于重载关键字的处理是不容易的,所以C++使用了另外一种方式来实现函数重载。

C++的函数重载和链接

由于在C语言中没有重载,所以编译器对函数的链接也十分简单:函数的名字是customMax,那么其在目标文件中的符号名字就是customMax,我们可以来看一下:
对于函数:

1
2
3
4
// customMax.c
int customMax(int x,int y){
return x>=y?x:y;
}

根据在上一篇文章中提到的生成目标代码的方法,来对其进行操作:

1
2
# 生成customMax.c的目标文件customMax.o
$ gcc -c customMax.c -o customMax.o

可以通过gcc工具链中的nm来查看目标文件中的符号信息:

1
2
3
# 查看customMax.o中的符号信息
$ nm customMax.o
0000000000000000 T customMax

函数符号类型:

  • T 该符号放在当前目标文件的代码段中,通常是那些全局非static函数
  • U 该符号在当前目标文件中未定义,需要从其他对象文件中链接进来

可以看到C语言中函数被编译器编译之后的符号仍然是函数名。但是在C++中这是个问题了,如果如同C语言这种简单的符号编译和链接方式,那么一个函数名怎么区分不同的重载版本?

1
2
3
// error: conflicting types for 'customMax'
int customMax(int x,int y);
double customMax(double x,double y);

在C中这两个函数编译出来的符号完全相同,从这一层来说,无法直接通过相同的符号名字(本身相同的符号名字都是具有歧义的)来区分一个不同的函数。
对于上面的问题,C++采取的方案的基本思想是:将类型信息编码传递到链接程序的名字里
即对于函数的类型信息(返回类型,参数类型)编码到编译之后的符号名字中(以下仅为解释,非实际编译器行为):

1
2
3
4
5
6
int customMax(int x,int y);
// 编译之后的符号名字可能为
_i_customMax_ii

double customMax(double,double);
_d_customMax_dd

来看一下编译器实际的行为。对于上面customMax的代码来使用C++的编译器(g++)来生成目标文件,且查看其中函数customMax的符号信息:

1
2
3
$ g++ -c customMax.c -o customMax.o
$ nm customMax.o
0000000000000000 T _Z3customMaxii

此时我们往里面添加一个重载函数double customMax(double,double):

1
2
3
4
5
// customMax.c
int customMax(int x,int y){/*...*/}
double customMax(double x,double y){
return x>=y?x:y;
}

再编译生目标文件,查看其中的符号信息:

1
2
3
4
$ g++ -c customMax.c -o customMax.o
$ nm customMax.o
000000000000001c T _Z9customMaxdd
0000000000000000 T _Z9customMaxii


可以看到,double custom(double,double)int custom(int,int)在目标文件中的符号信息并不一样,这是支持C++重载的实现原则。

使用gcc工具链里的c++filt可以将符号信息转换为原本的函数信息(至于为什么没有显示出函数的返回类型其原因是因为function Signatures,暂时按下不表,后面会讲到):

1
2
$ c++filt _Z3customMaxii
customMax(int, int)

可以看到同一个函数customMax通过C语言编译器和C++编译器编译出来的符号信息是不同的,编译器对函数名字生成符号的操作叫做**名称改编(name mangling)**,暂时先按下不表,后文详述。

C++函数重载与C的兼容性

但是这么做会有一个问题:与C的兼容性。C++之父在设计C++时把兼容C语言当做头等大事,并要求C++与C语言之间不应该具有无故的不兼容。因为函数重载与C造成的不直接兼容是必须要接受的,但是C++提供了一种间接兼容的方式。
考虑下面的问题:

1
2
// C code and using C compiler
int customMax(int x,int y){}

对于上面的函数customMax,它可能位于某个静态链接库或动态链接库中——即非源码形式库。
这样我们不能对其进行侵入式的修改,现在有一个问题:因为C编译器没有对函数名字到符号之间进行修改(函数名就是目标文件中的符号名),所以上面的代码所产生的符号信息是customMax,但是由于C++要兼容C的库,我想要在我的C++代码里调用这个链接库中的customMax函数会产生什么样的效果呢?
即使用C的编译器编译函数customMax的实现,用C++编译器编译对customMax的调用。

1
2
3
4
// customMax.c
int customMax(int x,double y){
return x>=y?x:y;
}

使用C编译器编译:

1
2
3
4
5
# C编译器编译出customMax的目标文件customMax.o
$ gcc -c customMax.c -o customMax.o
# 查看该目标文件中的符号信息
$ nm customMax.o
0000000000000000 T customMax

对于customMax的调用代码:

1
2
3
4
5
6
7
// main.c
extern int customMax(int,int);
int main(int argc,char* argv[])
{
customMax(11, 12);
return 0;
}

使用C++编译器编译:

1
2
3
4
5
$ g++ -c main.c -o main.o
# 查看目标文件中的符号信息
$ nm main.o
---------------- U _Z6customMaxii
0000000000000000 T main

gccsym-gxxsym-1

来尝试将他们链接一下:

1
2
$ g++ main.o customMax.o
main.o:main.c:(.text+0x1f): undefined reference to `customMax(int, int)'

这是因为main.o中的符号名_Z6customMaxii在链接时没有被找到,customMax.o中的符号是customMax,他们并不匹配,所以连接错误。如下图:
gxxsymbol link gccsymbol

extern "C"的引入

正是由于上面提到的原因,在C++的目标文件中不能直接连接到C库的目标文件中(这个目标文件包括.o以及使用C语言编译器生成的动静态链接库)。
这里C++调用C库的问题在于:如何把一个C++调用的函数“伪装成”C函数。为了实现这个,必须显式说明相关的函数具有C的链接,否则C++的编译器就会假定是C++函数,对其名字进行编码(name mangling)。
C++引入了一个链接描述符:

1
2
3
extern "C"{
int customMax(int,int);
}

这种链接方式并不影响customMax函数本来的语义,它只是告诉编译器,在目标代码里,对customMax函数应该使用C编译器的符号命名规则。
就像前面的main.c中的代码,我们可以对其进行改写:

1
2
3
4
5
6
7
8
// main.c
// extern int customMax(int,int);
extern "C" int customMax(int,int);
int main(int argc,char* argv[])
{
customMax(11, 12);
return 0;
}

这样我们在对其使用C++的编译器编译,并查看其customMax函数的符号信息:

1
2
3
4
$ g++ -c main.c -o main.o
$ nm main.o
---------------- U customMax
0000000000000000 T main

可以看到,customMax的符号信息并不是_Z6customMaxii了,那么,再使用先前使用C编译器编译的customMax.o进行链接:

1
2
# OK
$ g++ main.o customMax.o

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
2
3
// error: conflicting types for 'func'
int func(int x){}
double func(int x){}

而且,函数的签名也不包括CV-qualifiers,这意味着重载的函数起参数类型如果只是具有CV-qualifiers的区别,它也不是同在而是重定义。

1
2
3
// error: conflicting types for 'func'
int func(int x){}
int func(const int x){}
  • 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
2
3
4
5
// customMax.cpp
#include "custonmMax.h"
int customMax(int x,int y){
return x>=y?x:y;
}

对其编译生成目标文件之后,可以查看其中的符号信息:

1
2
3
$ g++ -c customMax.cpp -o customMax.o
$ nm customMax.o
00000050 T _Z9customMaxii

在得到该目标文件中customMax的符号信息之后,可以在customMax.h中写一个条件包含:

1
2
3
4
5
6
7
8
9
10
// customMax.h
#ifndef __CUSTOM_MAX_H__
#define __CUSTOM_MAX_H__

#ifdef __cplusplus
int customMax(int,int);
#else
int _Z9customMaxii(int,int);
#endif
#endif

就是当我们使用一个C++编译器来编译一个包含了customMax.h的代码时就会被包含进来int customMax(int,int);,而如果是使用非C++编译器编译时会包含int _Z9customMaxii(int,int);

使用再写一个调用customMax.o中的customMax的C代码(并且使用C语言编译器编译):

1
2
3
4
5
6
// main.c
#include "customMax.h"
int main(int argc,char* argv[])
{
_Z9customMaxii(12,14);
}

对于上面的main.c使用C语言编译器编译得到main.o:

1
2
3
4
$ gcc -c main.c -o main.o
$ nm main.o
-------- U _Z9customMaxii
00000000 T main

然后将与使用C++编译器编译的customMax.cpp得到的目标文件customMax.o链接起来:

1
2
# 链接成功
$ gcc main.o customMax.o -o main.exe

但是这里有个非常麻烦的地方在于:要先获取到目标文件中的符号名再去修改包含的customMax.h中的声明信息,非常繁琐,这种方式比较适用于在无法获取需要链接的库的源码的情况下,自己手动导出符号调用

包装函数

如果我们可以获取到需要链接的库的源码,可以添加一个包装函数:使用extern "C"包装一个目标函数(仍然使用customMax.cpp举例):

1
2
3
4
5
6
7
8
9
10
11
12
#include "customMax.h"

int customMax(int x,int y){
return x>=y?x:y;
}

// extern C call
extern "C"{
int customMaxii(int x,int y){
return customMax(x,y);
}
}

这样在使用C++编译器编译该代码的时候会在目标文件中看到两个符号信息:

1
2
3
$ g++ -c customMax.cpp -o customMax.o
00000000 T _Z9customMaxii
00000040 T customMaxii

然后就可以在customMax.h中写一个条件编译:

1
2
3
4
5
6
7
8
9
#ifndef __CUSTOM_MAX_H__
#define __CUSTOM_MAX_H__

#ifdef __cplusplus
int customMax(int,int);
#else
int customMaxii(int,int);
#endif
#endif

与上一种方式不同的是,我们不需要手动获取customMax.o中的符号信息了,而是在库中导出一个特定的C语言接口。

然后同样使用main.c来调用:

1
2
3
4
5
#include "customMax.h"
int main(int argc,char* argv[])
{
customMaxii(12,14);
}

同样可以编译并且与使用C++编译器编译的customMax.cpp链接成功。

注意:如果你在customMax.cpp中使用了C++的库,在链接时也必须把其的符号文件/静态/动态链接库指定进来,不然会出现未定义标识符错误。
因为C++编译器和C语言编译器在链接是使用的是不同的库(静/动态),在使用C++编译器并且链接是不会出现这样的问题,因为C++编译器会自动(隐式链接)从C++编译器的路径的库中扫描该符号信息。

例如,我在customMax.cppcustomMax函数中使用C++标准库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "customMax.h"
#include <iostream>

int customMax(int x,int y){
std::cout<<x<<std::endl<<y<<std::endl;
return x>=y?x:y;
}

// extern C call
extern "C"{
int customMaxii(int x,int y){
return customMax(x,y);
}
}

对其使用C++编译器编译时不会报错,但是在与main.o链接时会提示未定义标识符错误:

外部参考

更新日志

2017.04.12

  • 增加代码示例
  • 优化部分措辞,修改部分函数的命名
  • 更新图片,使之对应上面的修改

2017.04.24

  • 增加”C语言中调用C++函数”
全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:为什么需要extern "C"?
文章作者:查利鹏
发布时间:2017年04月11日 22时20分
更新时间:2017年04月24日 01时14分
本文字数:本文一共有4.9k字
原始链接:https://imzlp.com/posts/5392/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!