当前位置: 首页 > web >正文

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.cMain.cpp 编译中间文件example.oMain.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

http://www.xdnf.cn/news/14538.html

相关文章:

  • 盟接之桥EDI软件:开启制造业数据对接与协同的新纪元
  • Requests源码分析01:运行tests
  • 结构学习的理论(第1、2章)
  • OpenKylin安装运行ssh及sftp服务
  • 缓冲区技术
  • SCAU大数据技术原理雨课堂测验2
  • NodeJS11和10以及之前的版本,关键差异?
  • 大模型<闲谈>
  • 6.14打卡
  • 解决虚拟环境中文绘图显示问题
  • 【DVWA系列】——SQL注入——low详细教程
  • CFD仿真硬件选型建议
  • Python高效操作MySQL数据库
  • 2025最新Nvm安装教程
  • ceil方法
  • linux多线程之可重入函数
  • 618背后的电商逻辑重构:从价格血战到价值共生
  • nlp和大模型
  • 深入剖析AI大模型:GPU在大模型训练与推理的性能抉择
  • gpfs的安装配置与部署
  • C语言:Linux libc和glibc的历史
  • Java的String
  • GitHub又打不开了怎么办?git pull push失败怎么办?
  • SpringBoot 全面深入学习指南
  • 【系统分析师】2011年真题:综合知识-答案及详解
  • k8s-pod-01的学习
  • AI for 数据分析:技术演进与应用实践
  • 汇川IS620N伺服驱动器如何通过etherCAT主站转profinet网关与西门子1200plc通讯
  • STL容器分类总结
  • 快速取模指数算法:密码学的核心引擎