C++与C如何相互调用
C++ 与 C 如何相互调用
C++ 是在 C 语言的基础上发展而来的,意味着一个有效的 C 程序也是一个有效的 C++ 程序。即使这样 C 和 C++ 也是两种不同的编程语言,编译上 .c
采用 gcc 编译,.cpp
采用 g++ 编译,两者编译规则不同,所以调用上需要一些处理。
1. 示例演示
演示说明 C/C++ 调用函数的形式,先定义 .c
文件 example.c,定义函数 example(),执行后在终端输出 example();
。
/**@file example.c*/
#include "example.h"
#include <stdio.h>int32_t example()
{printf("example();");return 0;
}
再定义 .h
文件,声明一下 example() 函数。
/**@file example.h*/
#ifndef __EXAMPLE_H__
#define __EXAMPLE_H__#include <stdint.h>int32_t example();#endif /*__EXAMPLE_H__*/
定义 .cpp
文件 Main.cpp,定义主函数 main(),执行后调用 .c
的 example() 函数并在终端输出 main();
。
/**@file Main.cpp*/
#include "example.h"
#include <stdio.h>
#include "inc.h"int32_t main()
{printf("main();");example();for (;;) {}return 0;
}
再定义 .h
文件,声明一下 main() 函数。
/**@file inc.h*/
#ifndef __INC_H__
#define __INC_H__#include <stdint.h>int32_t main();#endif /* __INC_H__ */
编译示例程序,输出错误编译信息,提示 Main.cpp 调用的函数 example() 没有定义,具体如下。
C:\Users\20220615\Desktop\example\build>make
Consolidate compiler generated dependencies of target Main.exe
[ 33%] Linking CXX executable Main.exe
Memory region Used Size Region Size %age Used
CMakeFiles/Main.exe.dir/Main.cpp.obj: In function `main':
C:/Users/20220615/Desktop/example/Main.cpp:13: undefined reference to `example()'
collect2.exe: error: ld returned 1 exit status
make[2]: *** [CMakeFiles\Main.exe.dir\build.make:112: Main.exe] Error 1
make[1]: *** [CMakeFiles\Makefile2:82: CMakeFiles/Main.exe.dir/all] Error 2
make: *** [Makefile:90: all] Error 2
2. 示例分析
示例程序中 example() 已定义,并且 Main.cpp
也包含了 example 的 .h
文件,然而却提示未定义,下面用 objdump
命令查看 example.c
和 Main.cpp
编译中间文件example.o
和 Main.o
的符号表。
没强制没要求编译中间文件后缀名,所以可能被定义为其他后缀,比如xxx.c.obj,但本质还是 xxx.o。
查看 example.c.obj 符号表。
objdump -t example.c.obj
可以看到 example.c 确实定义了 example() 函数,还调用了 printf() 函数。
查看 Main.cpp.obj 符号表。
objdump -t Main.cpp.obj
可以看到 Main.cpp 定义了 main() 函数,调用了 printf(),_Z7examplev() 函数。
Main.cpp 编译后调用 example 的名称变成 _Z7examplev,和实际定义不一致,所以 Main.cpp 认为 example 未定义。
3. C++ 命名重整技术
为什么 .cpp
编译后函数名称会发生改变?这里要说到 C++ 的命名重整技术,也叫命名粉碎(Name-mangling)。
Name Mangling 是一种在编译过程中,将函数,变量名,返回值,通过一定算法,重新改编的机制,然后存储到二进制目标文件的符号表 SYMBOL TABLE。
为什么要粉碎命名?
Name Mangling 作用简单来说就是编译器为了区分同名符号,把同名符号重新修饰为一个全局唯一的符号。
C++ 允许两个函数是同名的,只要它们的参数列表不同即可。
而 C++ 的函数重载机制就是基于此实现的,重载函数的名称相同,为了让链接器在工作的时候不陷入困惑,将所有名字重新编码,生成全局唯一名称不重复的函数,让链接器能够准确识别每个名字所对应的对象。
所以重载函数就是编译器在编译阶段自动根据参数,以及类型给同名函数取不同的名称来实现的(C++ 的 namespace 原理也是相同)。
粉碎规则是什么?
Name-mangling 不是一个非常新的技术,在 C 语言中也有,我们在汇编 C 语言时经常看到的以 下划线 _
开头的函数名,其实就是 C 编译器将函数名进行了Name-mangling。 但在 C++ 中要复杂的多,因为 C++ 支持 overload 和override,这就要求 C++ 编译器必须要有 Name-mangling 的规则。
(1) 在 C++ 类外的变量名称完全不进行调整,名为 arr
的数组调整后还是 arr
。调整不与类相关的函数名称是根据参数类型用 __F
后缀和一串代表参数类型的字母组成的。
(2) 参数类型会被简写,规则如下。
void:v, int:i, float:f, char:c, double:d, varages:e,
long:l, unsigned:U, const:C, volatile:V, function:f
例如: 函数 func(float,int,unsigned char)
可能粉碎为 func__FfiUc
。
(3) 类名称认为是有类型的,粉碎成名称长度结合名称,如 Pair
粉碎为 4Pair
。
(4) 多个层次的内部类名称用 Q
来编码,后面跟随数字标明层次数,随后组合类名,和名称长度。
例如:First::Second::Third
变成了 Q35First6Second5Third
。
而一个有两个类参数的函数 f(Pair,First::Second::Third)
变成f__F4PairQ35First6Second5Third
。
(5) 类成员函数粉碎规则为函数名,结合下划线,类名称,F
和参数类型简写。
例如:cl::fn(void)
粉碎为 fn__2clFv
,所有操作符也都粉碎成 4-5 个字符的名字,如 __ml
代表 *
乘法操作。
命名粉碎 C++ 是必须的,不然就无法实现重载,但粉碎规则没有统一标准,各编译器之间不兼容,这也是 C++ 兼容问题显著的原因。
4. extern “C”
C 是单一命名空间的,不允许函数重载,也就是说在一个编译和链接的范围之内,C 不允许同名对象。所以 C 不用对名字进行处理,源码是什么编译后也就是什么。
所以 C 和 C++ 中对同一个函数经过编译后生成的函数名是不同的,这就会导致一个问题,如果在 C++ 中调用一个使用 C 语言编写模块中的某个函数,C++ 是按照 C++ 的名称修饰方式来查找并链接这个函数,就会发生链接错误。
例如: C 编译 void hello()
,最终函数依旧为 hello
,而 C++ 编译会把函数编译成 _Z4hellov。
所以这里要说到 C++ 引入的 extern "C"
语法,它用来告诉 g++ 在函数声明上使用 C 的调用约定处理函数名,以便在链接阶段能够正确解析函数名。
链接规范的用法有两种,可以单个声明链接规范,这样。
extern "C" void example();
可以一组声明链接规范,这样。
extern "C"
{int32_t hello();void example();
}
C 函数被 C 函数调用是不需要 extern "C"
修饰的,被 C++ 函数调用时才需要,所以 g++ 编译时加入 extern “C”,而 gcc 编译时跳过,所以可以使用条件编译控制 extern “C” 的作用,g++ 定义了特定宏 __cplusplus
。
#ifdef __cplusplus
extern "C" {
#endif#ifdef __cplusplus
}
#endif
使用条件编译一是 C 调用 C 不需要,当然二是 gcc 并不识别 "C"
这个关键字。
5. C++ 调用 C
调整前面 example 函数的头文件,增加 extern “C” 声明,告诉 g++ 应该按照 C 的方式来处理函数名。
/**@file example.h*/
#ifndef __EXAMPLE_H__
#define __EXAMPLE_H__#include <stdint.h>#ifdef __cplusplus
extern "C" {
#endifint32_t example();#ifdef __cplusplus
}
#endif#endif /*__EXAMPLE_H__*/
再次编译示例程序,Main.cpp 不再提示调用的函数 example() 没有定义。
C:\Users\20220615\Desktop\example\build>make
[ 33%] Building CXX object CMakeFiles/Main.exe.dir/Main.cpp.obj
[ 66%] Building C object CMakeFiles/Main.exe.dir/example.c.obj
[100%] Linking CXX executable Main.exe
Memory region Used Size Region Size %age Used
[100%] Built target Main.exe
另一种方式是应用单个声明链接规范,直接在调用之前,临时进行声明,如下。
/**@file Main.cpp*/
#include <stdio.h>
#include "inc.h"extern "C" int32_t example();int32_t main()
{printf("main();");example();for (;;) {}return 0;
}
6. C 调用 C++
演示说明 C 调用 C++ 函数的形式,先定义 .cpp
文件 example.cpp,定义函数 example(),执行后在终端输出 example();
。
/**@file example.c*/
#include "example.h"
#include <stdio.h>int32_t example()
{printf("example();");return 0;
}
再定义 .h
文件,声明一下 example() 函数,使用 extern “C” 声明告诉 g++ 把 C++ 函数按照 C 的方式进行处理。
/**@file example.h*/
#ifndef __EXAMPLE_H__
#define __EXAMPLE_H__#include <stdint.h>#ifdef __cplusplus
extern "C" {
#endifint32_t example();#ifdef __cplusplus
}
#endif#endif /*__EXAMPLE_H__*/
定义 .c
文件 Main.c,定义主函数 main(),执行后调用 .cpp
的 example() 函数并在终端输出 main();
。
/**@file Main.cpp*/
#include "example.h"
#include <stdio.h>
#include "inc.h"int32_t main()
{printf("main();");example();for (;;) {}return 0;
}
再定义 .h
文件,声明一下 main() 函数。
/**@file inc.h*/
#ifndef __INC_H__
#define __INC_H__#include <stdint.h>int32_t main();#endif /* __INC_H__ */
注意:对于 C 调用 C++ 的情况,C 中没有 extern “C” 规则,您需要在 C++ 代码中使用 extern “C” 确保 C++ 函数按照 C 的方式进行链接,同时在 C 代码中包含相应的头文件并调用这些函数。
7. C 调用 C++ 成员函数
在 C 与 C++ 互相调用,是将函数用 extern “C” 声明,但只适用于非成员函数,如果要调用 C++ 成员函数,则需要提供一个简单的全局包装(wrapper)函数。
class Example {virtual int16_t _example(int16_t);
};int16_t BeCall(Example * ex_p, int16_t val)
{return ex_p->_example(val);
}
定义 .h
文件声明把 C++ 函数处理成 C 函数(在 C++ 代码中使用 extern “C” 确保 包装函数 ByCall 按照 C 的方式进行链接,才能够被 C 函数调用)。
extern "C" int16_t BeCall(Example * ex_p, int16_t val);
定义 .c
文件定义函数 hello(),执行后调用 .cpp
成员函数的包装函数 BeCall()。
void _for_call(Example * ex_p, int16_t val)
{int16_t d = BeCall(ex_p, val);
}
8. 总结
(1) extern “C” 是什么?
是 C++ 引入的链接规范(linkage specification)的概念,表示法为 extern "Language String"
,C++ 编译器支持的 Language String 有 “C/C++” ,链接规范的作用是告诉 g++ 对于所有使用了链接规范进行修饰的声明或定义,应该按照指定语言的方式来处理。
(2) 为什么不把 include 语句放进 extern “C” 中?
因为 include 文件里面还有 extern “C” 存在,会造成 extern “C” 嵌套,当嵌套发生以内层为准时就会出现问题。
详细查看:
https://www.cnblogs.com/lizhenghn/p/3661643.html
https://www.cnblogs.com/skynet/archive/2010/09/05/1818636.html