一、混合编程杂绪
1.1 为什么需要混合编程
- Cpp是从C演变过来的,C有很多优秀成熟项目和库,没必要在C++中重写,C++程序可以直接调用调用
- 庞大项目划分后一部分适合用C(底层),一部分适合用C++(中间层、上层)
1.2 混合编程的支撑
- 编译型程序的编译过程:源文件->目标(库)文件->可执行程序->镜像文件
- 任何编程语言执行时都必须是可执行程序,所以都必须先被编译成目标文件
- 混合编程的“混合”操作发生在链接这一步
1.3 C++和C混合编程出现的问题
C++和C都是编译型语言,互相混合相对容易。但是:C++支持函数名重载,而C不支持,因此编译器生成目标文件时,函数名在目标文件中的临时内部名称规则不同,导致链接时符号对不上,如下所示:
创建测试文件:
touch cadd.c cppadd.cpp add.h Makefile
源文件和头文件的内容如下,其中cadd.c
和appadd.c
的内容都是一致的,均是库函数中声明的函数的定义
我们分别把.c源文件和.cpp源文件做成静态链接库然后把库进行反汇编
dec:
objdump -d libcadd.a > libcadd.i
objdump -d libcppadd.a > libcppadd.i
对比得到的反汇编文件,可以发现同一个函数分别在gcc
和g++
下编译后生成的二进制代码其实是一样的。而临时内部名称是不同的:在gcc
中,函数名保持不变;而g++
中,还在函数名后增加了参数类型(add -> addii
),如下图所示:
这时如果用一个.c
文件和g++
编译制作的静态库进行gcc
编译链接或用一个.cpp
文件和gcc
编译制作的库进行g++
编译链接都会链接错误:
#include "add.h"
int main(void)
{
int x = 1, y = 1;
add(x, y);
return 0;
}
这就是因为主函数#include "add.h"
中的add
(预编译时直接替换相关内容)与静态库中的add
用了不同的编译环境,导致标文件中的临时内部名称不同所致无法正确链接!
二、C和C++混合编程的解决方案
通过前面的研究我们可以知道C和C++本质上是可以混合编程的,但是生成的中间符号名称不同,所以链接器在进行链接时会出问题,那如何解决呢?
2.1 先谈谈__cplusplus
__cplusplus
是C++中预定义的一个标准宏,为长整型,其值为cpp标准的年月;在C中是未进行定义的,可以用来检测编译环境
#include <stdio.h>
int main(void)
{
#ifdef __cplusplus
printf("G++ %ld.\n", __cplusplus);
#else
printf("GCC.\n");
#endif
return 0;
}
2.2 extern "C"{}
使用extern "C"{}
可以使{}
中的内容用C的标准来编译,我们将1.3
中的add.h
中的内容改为:
#ifndef __ADD_H__
#define __ADD_H__
extern "C"
{
int add(int a, int b);
}
#endif
然后和cppadd.cpp
一起制作成静态库然后反编译,我们可以发现虽然用的g++
进行编译,反编译得到的文件中add
的临时名称还是add
,而不是_Z3addii
2.3 解决方案
通过2.2
我们可以知道在C++的头文件中只要把函数的声明放在extern "C"{}
的大括号范围之内,就可以让g++
在编译这个函数时生成中间符号名时按照C的规则而不是按照C++的规则,所以这样的函数就可以和C的库进行共同链接。但是,在C语言中,extern "C"{}
是未被定义的
所以我们将__cplusplus和extern “C”{}结合使用,通常有以下两种情况:
三、两种常见情况
3.1 同一个项目中C是库,C++是源码,C++调用C
这是最常见的情况,我们只需要在C的头文件中加上extern "C"
的声明,在C++
中直接包含头文件调用。后继直接用g++编译链接即可,如下所示(mian.cpp
和C库均来自1.3
):
add.h
中的内容更改如下:
#ifndef __ADD_H__
#define __ADD_H__
#ifdef __cplusplus
extern "C"
{
#endif
int add(int a, int b);
#ifdef __cplusplus
}
#endif
#endif
return.sh
中内容如下:
#!/bin/sh
g++ main.c -lcadd -L. -o main.elf
./main.elf
echo $?
根据结果我们可以看到程序进行了成功的编译、链接、执行:
如果我们同时有C和Cpp的源码,我们也是只需要在C的.h
库函数中加入__cplusplus
和extern "C"{}
,对于Cpp的.hpp
文件不做任何改动,最后统一进行g++编译即可
3.2 同一个项目中C++是库,C是源码,C调用C++
如果我们只有C++库而源码是C,我们最后只能用gcc编译链接,这带来了很多麻烦:
g++
和gcc
的编译时符号差异- c++支持很多c并不支持的特性,如函数重载
而C++继承了C的所有特性,也就是说C编译时无法直接像C++一样使用特定的语句实现编译Cpp时按照C的规则。但是,我们可以用cpp写一层封装层,加上extern "C"
,用g++
编译成静态库,这个时候我们在C源码中就可以直接调用我们的静态库,用gcc
编译链接!
我们还是使用1.3
中制作的静态库,我们检查发现此时libcppadd.a
中add
的临时符号是_Z3addii
我们制作封装层,创建文件
touch cppaddwrapper.cpp cppaddwrapper.hpp
其中,cppaddwrapper.hpp
中的内容如下:
#ifndef __CPP_ADDWRAPPER_HPP__
#define __CPP_ADDWRAPPER_HPP__
#ifdef __cplusplus
extern "C"
{
#endif
int addwrapper(int a, int b);
#ifdef __cplusplus
}
#endif
#endif
cppaddwrapper.cpp
中的内容如下
#include "add.hpp"
#include "cppaddwrapper.hpp"
int addwrapper(int a, int b)
{
add(a, b);
}
我们只需要在这个函数中对add进行调用即可,我们将封装层用g++
和ar
制作成静态封装库,再进行反编译,结果如下:
因为在addwrapper
函数的声明中加了extern "C"
,故临时符号没有改变。由于用的是g++编译,而该头文件包含中的add.hpp
的add
函数没有extern "C"
声明,所以调用时使用的add
的临时名称还是会变成_Z3addii
,和静态库中的临时名称相匹配,所以在反汇编代码callq 1d <addwrapper+0x1d>
中发生了跳转。第一个参数1d
就是add
函数的地址入栈,尖括号里面的是偏移地址,因为我们使用的是静态库,无需重定位,所以偏移地址也是0x1d
。
这个时候,我们就可以在我们自己的c文件中直接调用静态封装库中的函数,和封装库一起用gcc
进行链接了!如下建立测试文件夹,将头文件和静态库移入文件夹中:
main.c中的测试代码如下:
#include "cppaddwrapper.hpp"
int main(void)
{
addwarpper(1, 1);
return 0;
}
shell
脚本中的内容如下:需要注意的是由于封装层的静态库是使用了C++库的,所以在链接的时候也需要用-lxx
指定静态库
#!/bin/sh
gcc main.c -lcppaddwrapper -lcppadd -L. -o main.elf
./main.elf
echo $?
根据main函数的返回值我们可以知道程序成功地进行了编译链接执行
在只有C++库而源码是C的情况下,使用封装层的技巧就是封装层使是用g++编译,但是只在封装头文件函数声明中加了extern "C"
,而调用的C++静态库函数的#include
未加,因此调用的函数还是按C++的规则解析,制作库时可以实现连接。在C源码中调用封装库,巧妙避免了直接使用C++
库由于临时符号不同导致的链接错误