深思: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博客

        

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构】堆的实现

目录 1. 前言2. 堆的实现2.1 初始化2.2 插入2.2.1 分析2.2.1.1 情况一2.2.1.2 情况二2.2.1.3 情况三 2.2.2 插入代码实现2.2.2.1 向上调整代码 2.3 删除2.3.1 分析2.3.2 删除代码实现2.3.2.1 向下调整代码 2.4 找根节点数据2.5 元素个数2.6 判空2.7 销毁 3. 源代码3.1 Heap.h3.…

许战海战略文库|主品牌升级为产业技术品牌,引领企业全球化发展

在当今高速发展的全球经济中&#xff0c;企业品牌已经成为其核心资产之一。这不仅仅是因为品牌可以为消费者带来识别度&#xff0c;更重要的是&#xff0c;它们可以为企业带来深厚的竞争壁垒。但对于许多企业来说&#xff0c;特别是技术密集型企业&#xff0c;仅仅依靠主品牌的…

如何让消费者接受品牌,口碑营销怎么做?

当新品牌进入小红书时&#xff0c;如何进行口碑营销是一个重要的问题。很多新品牌在刚刚进入小红书时&#xff0c;对于一些敏感时机把握的不准其实本质上&#xff0c;就是不明白什么阶段该做什么事。今天分享的就是如何让消费者接受品牌&#xff0c;口碑营销怎么做&#xff1f;…

NX二次开发UF_CURVE_ask_spline_data 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CURVE_ask_spline_data Defined in: uf_curve.h int UF_CURVE_ask_spline_data(tag_t spline_tag, UF_CURVE_spline_p_t spline_data ) overview 概述 Reads the spline data a…

Java核心知识点整理大全18-笔记

Java核心知识点整理大全-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全2-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全3-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全4-笔记-CSDN博客 Java核心知识点整理大全5-笔记-CSDN博客 Java核心知识点整理大全6…

Linux驱动开发——网络设备驱动(理论篇)

目录 一、前言 二、网络层次结构 三、网络设备驱动核心数据结构和函数 一、前言 网络设备驱动是 Linux 的第三大类驱动&#xff0c;也是我们学习的最后一类 Linux 驱动。这里我们首先简单学习一下网络协议层次结构&#xff0c;然后简单讨论 Linux 内核中网络实现的层次结构。…

接口测试工具(Jmeter)必学技巧

安装 使用JMeter的前提需要安装JDK&#xff0c;需要JDK1.7以上版本目前在用的是JMeter5.2版本&#xff0c;大家可自行下载解压使用 运行 进入解压路径如E: \apache-jmeter-5.2\bin&#xff0c;双击jmeter.bat启动运行 启动后默认为英文版本&#xff0c;可通过Options – Cho…

【知网稳定检索】2024年应用经济学,管理科学与社会发展国际学术会议(AEMSS 2024)

2024年应用经济学&#xff0c;管理科学与社会发展国际学术会议&#xff08;AEMSS 2024&#xff09; 2024 International Conference on Applied Economics, Management Science and Social Development 2024年应用经济学&#xff0c;管理科学与社会发展国际学术会议&#xff…

uniapp在H5端实现PDF和视频的上传、预览、下载

上传 上传页面 <u-form-item :label"(form.ququ3 1 ? 参培 : form.ququ3 2 ? 授课 : ) 证明材料" prop"ququ6" required><u-button click"upload" slot"right" type"primary" icon"arrow-upward" t…

流媒体播放器EasyPlayer播放H.265与H.264时进度条样式异常该如何解决?

H5无插件流媒体播放器EasyPlayer属于一款高效、精炼、稳定且免费的流媒体播放器&#xff0c;可支持多种流媒体协议播放&#xff0c;可支持H.264与H.265编码格式&#xff0c;性能稳定、播放流畅&#xff0c;能支持WebSocket-FLV、HTTP-FLV&#xff0c;HLS&#xff08;m3u8&#…

网易云音乐7天黑胶VIP会员免费领取入口怎么领取网易云音乐黑胶VIP7天会员?

网易云音乐7天黑胶VIP会员免费领取入口怎么领取网易云音乐黑胶VIP7天会员&#xff1f; 1、百度搜索「词令」&#xff0c;在搜索框内输入词令「vip163」关键词直达口令&#xff0c;进入网易云音乐7天黑胶VIP会员免费领取入口&#xff1b; 2、输入网易云音乐黑胶VIP7天会员领取词…

AJAX技术-04-- 跨域说明

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 1 同源策略同源策略介绍规定要求 请求协议://域名:端口号 关于同源策略练习关于同源策略总结 2.JSONPJSONP原理说明关于JSONP优化 3.CORS介绍介绍不允许跨域说明跨域…

Cascader 级联选择器动态加载数据的回显

如果后端没有只返回第三级的id,而是同时把第三级的名字一起返回了&#xff0c;那么就可以通过下面的方法来实现 1.在级联选择器里面加上这句代码 placeholder"请选择" 2.注册一个字符串 pleasett:"" 3.赋值 如过后端返回的有第三级的选项名 直接进行赋…

【WP】Geek Challenge 2023 web 部分wp

官方出的题很好 学到很多东西 前面几道入门提就不写了 klf_ssti 目录扫描扫到一个robots.txt 打开存在hack路径&#xff0c;查看源码存在klf 传参,结合题目 就是ssti注入了&#xff0c;然后使用tplmap工具发现是盲注&#xff0c;我们这里直接用脚本找popen&#xff1a; im…

一键修复0xc000007b错误代码,科普关于0xc000007b错误的原因

最近很多用户都有遇到过0xc000007b错误的问题&#xff0c;出现这样的问题想必大家都会手足无措吧&#xff0c;其实解决这样的问题也有很简单的解决方法&#xff0c;这篇文章就来教大家如何一键修复0xc000007b&#xff0c;同时给大家科普一下关于0xc000007b错误的原因&#xff0…

Redis多机数据库

文章目录 Redis多机数据库一、主从复制1、旧版复制功能的实现a、同步b、命令传播 2、旧版复制功能的缺陷3、新版复制功能的实现a、部分同步功能b、复制实现步骤 4、心跳检测 二、哨兵1、Sentinel概念2、Sentinel初始化流程3、故障转移过程 三、集群1、几个概念2、集群创建流程a…

逆袭之战,线下门店如何在“?”萧条的情况下实现爆发增长?

未来几年&#xff0c;商业走势将受到全球经济形势、科技进步和消费者需求变化等多种因素的影响。随着经济复苏和消费者信心提高&#xff0c;消费市场将继续保持增长&#xff0c;品质化、个性化、智能化等将成为消费趋势。同时&#xff0c;线上购物将继续保持快速增长&#xff0…

Spring Data Redis切换底层Jedis 和 Lettuce实现

1 简介 Spring Data Redis是 Spring Data 系列的一部分&#xff0c;它提供了Spring应用程序对Redis的轻松配置和使用。它不仅提供了对Redis操作的高级抽象&#xff0c;还支持Jedis和Lettuce两种连接方式。 可通过简单的配置就能连接Redis&#xff0c;并且可以切换Jedis和Lett…

springboot+vue实现websocket通信实例,进入页面建立连接

springbootvue实现websocket通信实例 进入页面建立连接 前端代码&#xff1a; <template><div class"app-container"><el-form :model"queryParams" ref"queryForm" size"small" :inline"true" v-show&qu…

HarmonyOS应用开发者高级认证【题库答案】

HarmonyOS应用开发者基础认证【题库答案】 一、判断题 云函数打包完成后&#xff0c;需要到AppGallery Connect创建对应函数的触发器才可以在端侧中调用&#xff08;错&#xff09;在column和Row容器组件中&#xff0c;aligntems用于设置子组件在主轴方向上的对齐格式&#xf…