C++和C的混合编译的项目实践
简介
C++
语言的创建初衷是 “a better C”,但是这并不意味着 C++
中类似 C
语言的全局变量和函数所采用的编译和连接方式与 C
语言完全相同。作为一种欲与 C
兼容的语言, C++
保留了一部分过程式语言的特点(被世人称为"不彻底地面向对象"),因而它可以定义不属于任何类的全局变量和函数。但是, C++
毕竟是一种面向对象的程序设计语言,为了支持函数的重载, C++
对全局函数的处理方式与 C
有明显的不同。
本文将介绍如何通过 extern “C” 关键字在 C++
中支持 C
语言 和 在C
语言中如何支持 C++
。
某企业曾经给出如下的一道面试题
为什么标准头文件都有类似以下的结构?
//head.h
#ifndef HEAD_H
#define HEAD_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif
问题分析
- 这个头文件head.h可能在项目中被多个源文件包含(#include “head.h”),而对于一个大型项目来说,这些冗余可能导致错误,因为一个头文件包含类定义或inline函数,在一个源文件中head.h可能会被#include两次(如,a.h头文件包含了head.h,而在b.c文件中#include a.h和head.h)——这就会出错(在同一个源文件中一个结构体、类等被定义了两次)。
- 从逻辑观点和减少编译时间上,都要求去除这些冗余。然而让程序员去分析和去掉这些冗余,不仅枯燥且不太实际,最重要的是有时候又需要这种冗余来保证各个模块的独立。
为了解决这个问题,上面代码中的
#ifndef HEAD_H
#define HEAD_H
#endif
就起作用了。如果定义了HEAD_H,#ifndef/#endif之间的内容就被忽略掉。因此,编译时第一次看到head.h头文件,它的内容会被读取且给定HEAD_H一个值。之后再次看到head.h头文件时,HEAD_H就已经定义了,head.h的内容就不会再次被读取了。
那么下面这段代码的作用又是什么呢?
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
我们将在后面对此进行详细说明。
关于 extern “C”
前面的题目中的 __cplusplus
宏,这是C++中已经定义的宏,是用来识别编译器的,也就是说,将当前代码编译的时候,是否将代码作为 C++
进行编译。
首先从字面上分析extern “C”,它由两部分组成:extern关键字、“C”。下面我就从这两个方面来解读extern "C"的含义。
首先,被它修饰的目标是 extern
的;其次,被它修饰的目标是 C
的。
extern关键字
被 extern “C” 限定的函数或变量是 extern
类型的。
extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
被extern修饰的函数,需要在编译阶段去链接该目标文件,并且与extern对应的关键字是 static,被static修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其一般是不可能被extern “C”修饰的。
**注意:**例如语句 extern int a;
仅仅是对变量的声明,其并不是在定义变量 a
,声明变量并未为 a
分配内存空间。定义语句形式为 int a;
变量 a
在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。
被 extern “C” 修饰的变量和函数是按照 C
语言方式编译和连接的。
由于C++和C两种语言的亲密性,并且早期大量的库都是由C语言实现的,所以不可避免的会出现在C++程序中调用C的代码、C的程序中调用C++的代码,但是它们各自的编译和链接的规则是不同的。
函数名修饰
- 由于windows下vs的修饰规则过于复杂,而linux下GCc的修饰规则简单易懂,下面我们使用了gcc演示了这个修饰后的名字。
- 通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
分别使用C的编译器和C++的编译器去编译并获得一个可执行文件
使用C语言(gcc)编译器编译后结果
使用objdump -S 命令查看gcc生成的可执行文件:
使用C++编译器(g++)编译后结果
使用objdump -S 命令查看g++生成的可执行文件:
**linux:**修饰后的函数名= _Z + 函数名长度 + 形参类型首字母,Windows下也是相似的,细节上会有所不同,本质上都是通过函数参数信息去修饰函数名。
C++的编译和链接方式
采用g++编译完成后,函数的名字将会被修饰,编译器将函数的参数类型信息添加到修改后的名字中,因此当相同函数名的函数拥有不用类型的参数时,在g++编译器看来是不同的函数,而我们另一个模块中想要调用这些函数也就必须使用C++的规则去链接函数(找修饰后的函数名)才能找到函数的地址。
C的编译和链接方式
对于C程序,由于不支持重载,编译时函数是未加任何修饰的,而且链接时也是去寻找未经修饰的函数名。
C和C++直接混合编译时的链接错误
在C++程序,函数名是会被参数类型信息修饰的,这就造成了它们之间无法直接相互调用。
例如:
print(int)函数,使用g++编译时函数名会被修饰为 _Z5printi,而使用gcc编译时函数名则仍然是print,如果直接在C++中调用使用C编译规则的函数,会链接错误,因为它会去寻找 _Z5printi而不是 print。
【C和C++的编译和链接方式的不同】参考:
C++的函数重载
extern“C”的使用
extern "C"指令非常有用,因为C和C++的近亲关系。注意:extern "C"指令中的C,表示的一种编译和连接规约,而不是一种语言。
并且extern "C"指令仅指定编译和连接规约,并不影响语义,编译时仍是一个C++的程序,遵循C++的类型检查等规则。
对于下面的代码它们之间是有区别的
extern "C" void Add(int a, int b);
//指定Add函数应该根据C的编译和连接规约来链接
extern void Add(int a, int b);
//声明在Add是外部函数,链接的时候去调用Add函数
如果有很多内容要被加上extern “C”,你可以将它们放入extern “C”{ }中。
通过上面的分析,我们知道extern "C"的真实目的是实现类C和C++的混合编程,在C++源文件中的语句前面加上extern “C”,表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等。
那么混合编译首先要处理的问题就是要让我们所写的C++程序和C程序函数的编译时的修饰规则和链接时的修饰规则保持一致。
总共就有下面四种情况,也就是说一个C的库,应该能同时被C和C++调用,而一个C++的库也应能够同时兼容C和C++。
为了展示如上四种情况,我们分别建立一个C静态库和C++静态库。
C程序调用C的库,C++程序调用C++的库,这是理所应当的,因此我们关注的问题是如何交叉调用。
用法举例
静态库是什么
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常
之所以称为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库,因此体积较大
创建C静态库
我们以一个栈的静态库为例:
首先新建项目Stack_C
新建源文件和头文件
写好栈的代码
注意一定是C程序,即源文件后缀为c
更改输出文件类型
右键项目名称—>属性
更改为配置类型为静态库
生成静态库
查看是否生成成功
VS一般在项目路径下的x64\Debug路径下:
至此,静态库已经可以成功建立了。
再新建一个项目,写一个去调用该静态库实现的栈的程序(以括号匹配问题为例)
不过对于VS我们的静态库是默认不去使用的,因此我们需要将静态库的路径和库的名称分别添加到库目录和依赖项,才能让程序能去调用该静态库。
更改链接器配置
右键项目名—>点击属性
“属性面板“—>”配置属性”—> “链接器”—>”常规”,附加依赖库目录中输入,静态库所在目录;
增加库目录(路径为我们刚刚生成的静态库所在的Debug文件夹)
增加附加依赖项
名称为Stack_C项目生成的静态库名,一般是项目名 + .lib
“属性面板”—>”配置属性”—> “链接器”—>”输入”,附加依赖库中输入静态库名StaticLibrary.lib。
我们先尝试使用C程序来调用该静态库
新建项目
将源文件后缀改为c;包含上Stack_C项目(静态库项目)的头文件;点击生成解决方案;
成功生成,说明成功调用。
尝试使用C++程序调用C静态库
- 将源文件后缀改为cpp;
- 头文件保持不变;
- 点击生成解决方法
结果报错了:
这说明在链接的过程中出现了问题,也就是在我们的程序找不到静态库中函数的地址,原因是我们的静态库是C语言的,没有对函数进行修饰,但在我们的调用方是C++程序,在链接过程中找的是修饰过的函数名,因此无法找到函数的地址。
既然C语言的静态库只能按照C的规则去编译这些函数(即不修饰函数名),那么我们只要让C++程序按照C语言的链接规则(即找未经修饰的函数名)去找到函数名不就解决了?
两种思路:
- 改变C库的编译和链接方式为C++规则;
- 改变C++程序调用库函数的编译和链接方式为C的规则;
方法1是不行的,因为C语言中可没有extern “C++”这种东西,那么考虑方法2;
这时我们可以借助extern“C”改变C++程序的链接规则,让C++去按照C的规则去找函数名,即未经过任何修饰的函数名,那就一定能找到函数的地址,来去正确调用静态库。
在源文件test.cpp
使用extern “C”,去改变包含的头文件中的函数的链接规则
//调用库的的模块的头文件包含
extern "C"
{
#include"..\..\Stack_C\Stack_C\stack.h"
}
//程序的代码
//...
那么在test.cpp去链接函数时,就会直接去找原函数名。
这样就解决了。
还有一个一步到位的解决方法,利用条件编译,根据当前程序的类型,选择是否去执行extern “C”指令。
- 调用方是C程序,不做处理;
- 调用方是C++程序,需要使用extern“C”将程序改为C的链接规则;
//调用库的的模块的头文件包含
#ifdef __cplusplus//如果是c++程序,就执行extern “C”,使用C的链接方式,去找未经修饰的函数名
extern "C"{
#endif
#include"..\..\Stack_C\Stack_C\stack.h"
#ifdef __cplusplus
}
#endif
//程序的代码
//...
但是这样的处理不太好,我们作为调用方自然是想可以直接通过头文件包含的方式去使用库里的函数,因此采用下列方法,更改库的头文件函数声明为:
#ifdef __cplusplus//如果定义了宏__cplusplus就执行#ifdef 到 #endif之间的语句
extern "C"
{
#endif
void StackInit(struct Stack* s);
void StackPush(struct Stack* s, DataType x);
void StackPop(struct Stack* s);
DataType StackTop(struct Stack* s);
int StackSize(struct Stack* s);
void StackDestory(struct Stack* s);
bool StackEmpty(struct Stack* s);
#ifdef __cplusplus
}
#endif
这样的一段代码,无论是C++程序还是C程序都可以直接#include就能去调用该静态库了。
创建C++静态库
步骤和创建C的静态库相同,只不过要将项目中的源文件后缀改为cpp,就会生成一个C++的静态库,因此不再阐述。
创建完成后,我们仍使用刚刚的项目,并且添加C++静态库路径到库目录,添加C++静态库名称到附加依赖项,仍然以括号匹配问题为例去调用该库。(记得删除C静态库的库目录和附加依赖项,否则我们的程序有可能还会去调用C的静态库,这样我们就无法探究如何去调用C++静态库的问题了)
尝试使用C程序调用C++静态库
我们不着急调用,经过先前的经验,这里可以判断,C++的程序去调用C++的库一定是没问题的,但是C程序就不好说了,因此我们要搞定C程序调用C++库的情况,先搞清楚它们的差异:
这里的C++程序去调用函数是去寻找修饰后的函数名,C程序是去找未修饰的函数名,要想让它们保持一致有两个思路:
改变C程序的编译和链方式为C++的规则;改变C++静态库的编译方式为C的规则;
但是方法1是不行的,之前也说过,C语言中没有extern “C++”这种东西,那么考虑方法2;
对库的头文件中的函数做如下处理:
//用C的规则去搞库的编译和链接方式
extern "C"
{
void StackInit(struct Stack* s);
void StackPush(struct Stack* s, DataType x);
void StackPop(struct Stack* s);
DataType StackTop(struct Stack* s);
int StackSize(struct Stack* s);
void StackDestory(struct Stack* s);
bool StackEmpty(struct Stack* s);
}
那么现在C++的静态库的函数名都是没有经过修饰的。(C的规则)
但是我们去编译仍然报错:
error C2059: 语法错误:“字符串”
"StackInit”未定义;假设外部返回int
“StackPush”未定义;假设外部返回int
“StackEmpty”未定义;假设外部返回int
“StackTop”未定义;假设外部返回int
“StackPop”未定义;假设外部返回int
这是因为我们使用C程序时也包含了此头文件,但是C语言中无法识别extern“C”,因此报错。
我们尝试使用条件编译来决定是否使用extern“C”,根据调用方的不同改变函数链接规则:
- 调用方是C++程序,那么需要使用extern“C”将C++程序的函数链接规则变为C的;
- 调用方是C程序,不使用extern“C”语句;
因此我们做如下处理,将库的头文件中的函数声明加上:
#ifdef __cplusplus//如果定义了宏__cplusplus就执行#ifdef 到 #endif之间的语句
extern "C"
{
#endif
void StackInit(struct Stack* s);
void StackPush(struct Stack* s, DataType x);
void StackPop(struct Stack* s);
DataType StackTop(struct Stack* s);
int StackSize(struct Stack* s);
void StackDestory(struct Stack* s);
bool StackEmpty(struct Stack* s);
#ifdef __cplusplus
}
#endif
总结:C++和C之间的混合编译,为了消除函数名修饰规则不同的的差别,我们需要使用extern ”C“来改变C++的编译和连接方式。
但这样问题也随之而来:
C++的库就失去了函数重载的特性,如果库中有同名函数,那么就无法正确编译,因为按照C的方式去编译,函数名会冲突。
如何解决这个问题呢?
实际上这个问题无法解决,一旦选择了将某个函数指定了按照C的方式去编译链接,那么这个函数就已经失去了重载的特性了,不过Cpp的库中未被指定按照C的规则去编译和链接的那些函数,仍然可以被重载,并且具有C++的一切特性。
因此这个问题无解,只有通过避免“一刀切”的方法来保护那些我们想重载的函数,也就是说一部分库里的函数那就是实现给C程序调用的,我们就通过extern“C”改变它的编译和链接方式,而对于那些实现给C++程序调用的函数接口,我们不做任何处理,并且不暴露给C程序。
想要实现上述过程,我们需要在静态库项目中创建两个头文件libc.h
和libcpp.h
,libc.h
声明那些需要暴露给C程序的函数接口,并且使用上面介绍的条件编译和extern“C”,libcpp.h
声明那些暴露给给Cpp程序的函数接口,这样两个头文件的函数的链接规范互不相同,也互不干扰。只需要将lic.h
在C程序调用的地方使用#include
包含,libcpp.h
在C++程序调用的地方使用#include
包含即可使用。
因此C++库中哪个接口需要暴露给C,我们就用extern“C”修饰哪个接口。
总之,C的库可以给C程序和C++程序调用,而C++库也可以被C程序和C++程序调用
如果要满足这个库中所有的函数都能同时被C++和C调用,那么无论是C的库还是C++的库,最终这个库的编译和链接方式都只能是C的规范,因为C++可以使用C的链接规范但是C不能使用C++的链接规范,也就导致了如果库的链接规范是C++的,那么无论如何,C程序都无法调用。
值得一提的是C++程序中的函数可以使用两种链接规范,因此我们可以针对函数的使用场景来选择该函数的编译和链接规范,使得一部分函数保留C++的特性,但一部分函数就只能为了兼容C而牺牲C++的特性,想要既兼容C又保留C++的特性,这是做不到的。
到此这篇关于C++和C的混合编译的项目实践的文章就介绍到这了,更多相关C++和C混合编译内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
相关文章