深思:C与C++相互调用问题

背景

        上周,偶然看到同事愁眉苦脸的样子,便善意咨询了下发生了什么。简单沟通下,才知道他遇到了一个工程编译的问题,一直无法编译通过,困扰了他快一天时间。出于个人的求知欲和知识的渴望,我便主动与他一同分析,不一会儿就确认了问题原因。

        后经思考,类似的问题应该在工作中会经常遇到,而对于没有相关知识储备的工程师,一般很难会有排查和解决思路。于是乎想通过本篇文章,能够帮助大家了解为什么C与C++相互调用存在的问题以及如何解决这类问题

案例描述

        首先简单描述一下同事的案例场景:

        同事的项目工程依赖其他两个部门A和B提供的动态库libA.solibB.so。它们之间的关系是:libB.so 依赖 libA.so 生成。之后他再依赖libB.so库生成abupvdiapp可执行程序。在最终生成可执行程序时,会提示部分符号未定义。而这些符号是在libA.so。依赖关系如下图:

排查思路:

  1. 首先确认编译生成可执行程序abupvdiapp时,是否链接了libA.so。打开cmake 中的VERBOSE参数,发现的确有-lA -lB链接信息。
  2. 查看libB.so是否依赖libA.so。通过ldd libB.so查看,发现libB.so 并不依赖libA.so这是我第一个疑惑点
  3. 查看未定义符号adm_vdi_init是否在libA.so定义。结果是存在。
  4. 查看未定义符号adm_vdi_init是否被libB.so引用。结果是有被引用,但是符号不匹配

        其实到这里我已经知道什么原因了。后续在A.h的头文件中增加extern "C"即可。有兴趣的朋友可以按照下面的示例演示一遍,加深影响。

//A.c
#include <stdio.h>
int adm_vdi_init()
{
    printf("i'm adm_vdi_init\n");
}

//A.h
extern int adm_vdi_init();

//B.cpp
#include<A.h>
#include<stdio.h>
int adm_host_init()
{
    adm_vdi_init();
    printf("i'm adm_host_init\n");
    return 0;
}
//B.h
extern int adm_host_init();

//main.cpp
#include<B.h>
int main()
{
        adm_host_init();
        return 0;
}

编译流程如下:

修改后:

//A.h
#ifdef __cplusplus
extern "C" {
#endif
extern int adm_vdi_init();
#ifdef __cplusplus
}
#endif

        我相信有经验的朋友肯定已经知道问题的原因了,但是对于未接触相关案例的同学,估计还是一头雾水,特别是一直从事C语言开发的工程师,估计还不清楚发生了什么。如果你有同样的疑问,请继续往下阅读,一定不会让你失望。

原理

        我们知道模块之间的函数或全局变量的引用,其实就是对符号的引用。在链接过程中就是通过这个符号寻找对应的代码,实现上下文的跳转。

        在历史的长河中,先辈们发现随着项目工程的扩大,很容易出现不同模块定义了相同的全局变量或对外函数,导致符号相同的情况。这样就导致链接时,不知道应该链接到哪一个代码段。但是上述的现象是一个趋势,很难去避免。因此,出现了符号修饰的概念。

符号修饰即根据一定的规则,对源码中的符号进行修饰,进行区分

        在较新的GCC编译工具中,并不会对C 符号进行修饰。如上面的A.c文件中,定义了adm_vdi_init函数,编译之后的符号表中,依旧是adm_vdi_init

        C++因为支持类,继承,重载,名称空间等这些特定,因此GCC编译工具,会对C++进行符号修饰。C++符号的修饰规则可参考该GNU C++的符号改编机制介绍_gnu c++的符号装饰机制-CSDN博客

如图:B.cpp文件中adm_host_init函数经过修饰,变为了_Z13adm_host_initv。分析:

  1. _Z:属于标识
  2. 13:adm_host_init字符串长度
  3. adm_host_init :函数名
  4. v:参数void

        了解以上原理后,我们就明白为什么在A.h的头文件中增加extern "C",就编译通过了。libA.so是C语言,因此不会进行符号修饰。B.cpp 是C++,会进行符号修饰,编译器并不知道adm_vdi_init是否进行了符号修饰,所以它默认进行了修饰。所以libB.so引用的符号与libA.so的符号并不匹配。extern "C"就是明确告诉编译器,adm_vdi_init是C编译的,并没有进行符号修饰

C++调用 C

        同事的案例,其实就是C++(B.cpp)调用C(A.c)导致的问题。在这里我再简单总结一下:

        C 并不会进行符号修饰,而C++默认会对符号进行修饰,因此会导致C++ 认知中A.h 的符号与 A.o的实际符号不匹配,虽然动态库libB.so 已经能生成,但是在链接阶段,符号重定位时,就会出现undefined reference to错误。

        因此,C 代码以SDK的方式提供给外部使用时,应该在头文件中用extern "C"修饰。如:

#ifdef __cplusplus
extern "C" {
#endif

    .....

#ifdef __cplusplus
}
#endif

C调用C++

        C++ 调用C 的方式大家可能比较常见,因为C++更适合用于开发偏上层应用,C更适合底层开发。天然的存在依赖关系,所以在工作中也比较常见。

        而C 调用C++ 的场景就比较少了,一般是因为公司内部原因了。还是用上面的示例,将 main.cpp 改为main.c。

        该现象与我们预期相符,因为libB.so是C++ 编译的库,所以会对符号进行修饰。但是main.c 是C,gcc 并不会进行符号修饰。

        但是我们如何解决符号的问题呢?很难受,并没有extern "C++"的参数。在这里我提供三种思路:

最简单的方式

        最简单的方式就是将main.c 改为main.cpp 。要求GCC 以C++的方式去编译,自然会进行符号修饰。但是这样容易出现其他问题,因为C依赖的很多头文件,C++可能并不支持。为了做到兼容,可能会修改更多的代码。

封装一层C接口

如我们增加一个libC.so,代码如下:

// C.h
#ifdef __cplusplus
extern "C" {
#endif
extern int adm_C_init();
#ifdef __cplusplus
}
#endif

//C.cpp
#include<C.h>
#include<B.h>
int adm_C_init()
{
    adm_host_init();
    return 0;
}

// main.c
#include<C.h>
int main()
{
        adm_C_init();
        return 0;
}

        注意:其中C.cpp中显示声明了extern "C"修饰adm_C_init,因此libC.so中则不会对adm_C_init进行符号修饰。

直接引用被修饰的符号

        封装C接口的方式比较麻烦,还有一种就是比较简单,不易理解的方式。就是main.c 直接引用 libB.so中修饰过后的符号。比如,我们通过nm libB.so | grep adm_host_init查看修饰后的符号为_Z13adm_host_initv。我们可以这样修改main.c。

//main.c
extern int _Z13adm_host_initv();
int main()
{
        _Z13adm_host_initv();
        return 0;
}

总结

        综上所述,相信大家应该理解C 与 C++ 之间相互调用为什么存在一些困难的原因。工作中我们也应该注意一些事项。比如:C语言编译的SDK,对外提供头文件时,为了兼容c++,应该用extern "C"修饰

        希望本文能够给大家带来帮助,若有兴趣更深入了解相关编译知识,大家可以关注我的《程序员的自我修养》专栏。编译,链接,装载,库_谢艺华的博客-CSDN博客

        

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/195534.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PHP中关于func_get_args()方法

首先呢这个函数出现的是比较早的,大致应该是PHP4出现的, func_get_args — 返回一个包含函数参数列表的数组 说明 func_get_args(): array 获取函数参数列表的数组。 该函数可以配合 func_get_arg() 和 func_num_args() 一起使用&#xff0c;从而使得用户自定义函数可以接…

基于C#实现块状链表

在数据结构的世界里&#xff0c;我们会认识各种各样的数据结构&#xff0c;每一种数据结构都能解决相应领域的问题&#xff0c;当然每个数据结构&#xff0c;有他的优点&#xff0c;必然就有它的缺点&#xff0c;那么如何创造一种数据结构来将某两种数据结构进行扬长避短&#…

【数据结构/C++】栈和队列_循环队列

牺牲一个存储单元来判断队满。 #include<iostream> using namespace std; // 循环队列 #define MaxSize 10 typedef int ElemType; typedef struct {ElemType data[MaxSize];int front, rear; } SqQueue; // 初始化队列 void InitQueue(SqQueue &Q) {// 判断队空 Q.…

微服务知识大杂烩

1.什么是微服务? 微服务(Microservices)是一种软件架构风格,将一个大型应用程序划分为一组小型、自治且松耦合的服务。每个微服务负责执行特定的业务功能,并通过轻量级通信机制(如HTTP)相互协作。每个微服务可以独立开发、部署和扩展,使得应用程序更加灵活、可伸缩和可…

Milvus入门手册1.0

一、window环境搭建&#xff08;单机&#xff09; 1、docker安装 略 2、milvus安装 参考文档&#xff1a;https://milvus.io/docs/install_standalone-docker.md tips: &#xff08;1&#xff09;compose.yaml下载比较慢&#xff0c;可以在网络上找一份。 &#xff08;2&…

2023-11-26 LeetCode每日一题(统计子串中的唯一字符)

2023-11-26每日一题 一、题目编号 828. 统计子串中的唯一字符二、题目链接 点击跳转到题目位置 三、题目描述 我们定义了一个函数 countUniqueChars(s) 来统计字符串 s 中的唯一字符&#xff0c;并返回唯一字符的个数。 例如&#xff1a;s “LEETCODE” &#xff0c;则其…

Python字典合并

合并两个有部分key相同的字典&#xff0c;相同key保留两个字典中对应key的较大值。 (笔记模板由python脚本于2023年11月27日 18:12:15创建&#xff0c;本篇笔记适合熟悉Python字典的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Fr…

通过预定义颜色查找表上色_vtkLookupTable_vtkColorTransferFunction

开发环境&#xff1a; Windows 11 家庭中文版Microsoft Visual Studio Community 2019VTK-9.3.0.rc0vtk-example demo解决问题&#xff1a;通过颜色查找表给vtkPlaneSource上色 第一种技术是使用预定义颜色的查找表vtkLookupTable。这包括创建一个查找表并为其分配一组已命名的…

攻关眼科难题!第一届爱尔眼科-四川大学科研基金完成立项

当前我国眼科患者数量不断增长&#xff0c;人民群众对高质量的眼健康的需要不断攀升&#xff0c;而目前国内眼科医疗资源远不能满足需求&#xff0c;疑难眼病诊疗能力及学术科研体系建设仍有较大进步空间。基于此&#xff0c;爱尔眼科携手四川大学共同设立爱尔眼科-四川大学科研…

js moment时间范围拿到中间间隔时间

2023.11.27今天我学习了如何对只返回的开始时间和结束时间做处理&#xff0c;比如后端返回了&#xff1a; [time:{start:202301,end:202311}] 我们需要把中间的间隔渲染出来。 [202301,202302,202303,202304,202305,202306,202307,202308,202309,202310,202311] 利用moment…

搭建你自己的网盘-个人云存储的终极解决方案-nextcloud AIO(二)

今天接着上篇&#xff0c;我们继续来玩nextcloud AIO. 当我们看到这个页面的时候&#xff0c;则证明AIO已经安装好了&#xff0c;登录账号和密码在图上已经标注了。点击open your nextcloud 即可跳转到我们的域名的登录页。 输入用户名和密码后登录即可。 打开前台页面&#x…

【Dockerfile】将自己的项目构建成镜像部署运行

目录 1.Dockerfile 2.镜像结构 3.Dockerfile语法 4.构建Java项目 5.基于Java8构建项目 1.Dockerfile 常见的镜像在DockerHub就能找到&#xff0c;但是我们自己写的项目就必须自己构建镜像了。 而要自定义镜像&#xff0c;就必须先了解镜像的结构才行。 2.镜像结构 镜…

element table滚动条失效

问题描述:给el-table限制高度之后滚动条没了 给看看咋设置的&#xff1a; <el-table:data"tableData"style"width: 100%;"ref"table"max-height"400"sort-change"changeSort">对比了老半天找不出问题&#xff0c;最后…

上门预约互联网干洗店洗鞋店小程序开发

很多时候可能大家的衣服鞋子需要干洗&#xff0c;但又不想出门送去店里&#xff0c;大家就可以使用手机线上下单预约取货&#xff0c;会有专门的人上门来取衣服&#xff0c;让你能够轻松的进行洗护。 闪站侠洗衣洗鞋小程序&#xff0c;提供了足不出户就能预约人员上门去 衣送洗…

解决ansible批量加入新IP涉及known_hosts报错的问题

我们把一批新的IP加入到ansible的hosts文件&#xff0c;比如/etc/ansible/hosts&#xff0c;往往会有这样的提示&#xff0c; 因为本机的~/.ssh/known_hosts文件中并有fingerprint key串&#xff0c;使用ssh连接目标主机时&#xff0c;一般会提示是否将key字符串加入到~/.ssh/…

Java多线程核心技术二-synchronzied同步方法

1 概述 关键字synchronzied保障了原子性、可见性和有序性。 非线程安全问题会在多个线程对同一个对象中的同一个实例变量进行并发访问时发生&#xff0c;产生的后果就是“脏读”&#xff0c;也就是读取到的数据其实是被更改过的。而线程安全是指获取的实例变量的值是经过同步处…

蚂蚁庄园小课堂答题今日答案最新

蚂蚁庄园小课堂答题今日答案最新 温馨提醒&#xff1a;由于本文章会停留在一个固定的更新时间上&#xff0c;包含当前日期最新的支付宝蚂蚁庄园小课堂答题今日答案。如果您看到这篇文章已成为过去时&#xff0c;请按下面的方法进入查看天天保持更新的最新今日答案&#xff1b; …

Martin Fowler:数字化时代,远程与本地协同工作孰优孰劣?(2)| IDCF

作者&#xff1a;Martin Fowler 译者&#xff1a;冬哥 原文&#xff1a;https://martinfowler.com/articles/remote-or-co-located.html &#xff08;接上篇 &#xff09; 二、大多数人在同地办公时工作效率更高 与软件开发中的许多主题一样&#xff0c;我不能拿 100 个软…

NX二次开发UF_CURVE_convert_conic_to_std 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CURVE_convert_conic_to_std Defined in: uf_curve.h int UF_CURVE_convert_conic_to_std(UF_CURVE_genconic_p_t gen_conic_data, UF_CURVE_conic_t * conic_data, logical * se…

15:00面试,15:06就出来了,问的问题有点变态。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到8月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%…