C与C++相互调用的“底层原理“

news2024/12/23 9:30:04

在你的C语言代码中,不知能否看到类似下面的代码:

这好像没有什么问题,你应该还会想:“嗯⋯是啊,我们的代码都是这样写的,从来没有因此碰到过什么麻烦啊~”。

你说的没错,如果你的头文件从来没有被任何C++程序引用过的话。

这与C++有什么关系呢? 看看__cplusplus(注意前面是两个下划线) 的名字你就应该知道它与C++有很大关系。__cplusplus是一个C++规范规定的预定义宏。你可以信任的是:所有的现代C++编译器都预先定义了它;而所有C语言编译器则不会。另外,按照规范__cplusplus的值应该等于1 9 9 7 1 1 L ,然而不是所有的编译器都照此实现,比如g++编译器就将它的值定义为1。

所以,如果上述代码被C语言程序引用的话,它的内容就等价于下列代码。

在这种情况下,既然extern "C" { }经过预处理之后根本就不存在,那么它和#include指令之间的关系问题自然也就是无中生有。

extern "C"的前世今生

在C++编译器里,有一位暗黑破坏神,专门从事一份称作“名字粉碎”(name mangling)的工作。当把一个C++的源文件投入编译的时候,它就开始工作,把每一个它在源文件里看到的外部可见的名字粉碎的面目全非,然后存储到二进制目标文件的符号表里。

之所以在C++的世界里存在这样一个怪物,是因为C++允许对一个名字给予不同的定义,只要在语义上没有二义性就好。比如,你可以让两个函数是同名的,只要它们的参数列表不同即可,这就是函数重载(function overloading);甚至,你可以让两个函数的原型声明是完全相同的,只要它们所处的名字空间(namespace)不一样即可。事实上,当处于不同的名字空间时,所有的名字都是可以重复的,无论是函数名,变量名,还是类型名。

另外,C++程序的构造方式仍然继承了C语言的传统:编译器把每一个通过命令行指定的源代码文件看做一个独立的编译单元,生成目标文件;然后,链接器通过查找这些目标文件的符号表将它们链接在一起生成可执行程序。

编译和链接是两个阶段的事情;事实上,编译器和链接器是两个完全独立的工具。编译器可以通过语义分析知道那些同名的符号之间的差别;而链接器却只能通过目标文件符号表中保存的名字来识别对象。

所以,编译器进行名字粉碎的目的是为了让链接器在工作的时候不陷入困惑,将所有名字重新编码,生成全局唯一,不重复的新名字,让链接器能够准确识别每个名字所对应的对象。

但 C语言却是一门单一名字空间的语言,也不允许函数重载,也就是说,在一个编译和链接的范围之内,C语言不允许存在同名对象。比如,在一个编译单元内部,不允许存在同名的函数,无论这个函数是否用static修饰;在一个可执行程序对应的所有目标文件里,不允许存在同名对象,无论它代表一个全局变量,还是一个函数。所以,C语言编译器不需要对任何名字进行复杂的处理(或者仅仅对名字进行简单一致的修饰(decoration),比如在名字前面统一的加上单下划线_)。

C++的缔造者Bjarne Stroustrup在最初就把——能够兼容C,能够复用大量已经存在的C库——列为C++语言的重要目标。但两种语言的编译器对待名字的处理方式是不一致的,这就给链接过程带来了麻烦。

例如,现有一个名为my_handle.h的头文件,内容如下:

然后使用C语言编译器编译my_handle.c,生成目标文件my_handle.o。由于C语言编译器不对名字进行粉碎,所以在my_handle.o的符号表里,这三个函数的名字和源代码文件中的声明是一致的。

随后,我们想让一个C++程序调用这些函数,所以,它也包含了头文件my_handle.h。假设这个C++源代码文件的名字叫my_handle_client.cpp,其内容如下:

其中,粗体的部分就是那三个函数的名字被粉碎后的样子。

然后,为了让程序可以工作,你必须将my_handle.o和my_handle_client.o放在一起链接。由于在两个目标文件对于同一对象的命名不一样,链接器将报告相关的“符号未定义”错误。

为了解决这一问题,C++引入了链接规范(linkage specification)的概念,表示法为extern"language string",C++编译器普遍支持的"language string"有"C"和"C++",分别对应C语言和C++语言。

链接规范的作用是告诉C++编译:对于所有使用了链接规范进行修饰的声明或定义,应该按照指定语言的方式来处理,比如名字,调用习惯(calling convention)等等。

相关视频推荐

linux c/c++全栈开发实战教程(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)icon-default.png?t=N7T8https://www.bilibili.com/video/BV1oz4y1w7zM/

免费学习地址:Linux C/C++开发(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)

需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

链接规范的用法有两种

1、单个声明的链接规范,比如:

extern "C" void foo();

2、一组声明的链接规范,比如:

extern "C"
{
  void foo();
  int bar();
}

对我们之前的例子而言,如果我们把头文件my_handle.h的内容改成:

然后使用C++编译器重新编译my_handle_client.cpp,所生成目标文件my_handle_client.o中的符号表就变为:

从中我们可以看出,此时,用extern "C" 修饰了的声明,其生成的符号和C语言编译器生成的符号保持了一致。这样,当你再次把my_handle.o和my_handle_client.o放在一起链接的时候,就不会再有之前的“符号未定义”错误了。

但此时,如果你重新编译my_handle.c,C语言编译器将会报告“语法错误”,因为extern"C"是C++的语法,C语言编译器不认识它。此时,可以按照我们之前已经讨论的,使用宏__cplusplus来识别C和C++编译器。修改后的my_handle.h的代码如下:

小心门后的未知世界

在我们清楚了 extern "C" 的来历和用途之后,回到我们本来的话题上,为什么不能把#include 指令放置在 extern "C" { ... } 里面?

我们先来看一个例子,现有a.h,b.h,c.h以及foo.cpp,其中foo.cpp包含c.h,c.h包含b.h,b.h包含a.h,如下:

现使用C++编译器的预处理选项来编译foo.cpp,得到下面的结果:

正如你看到的,当你把#include指令放置在extern "C" { }里的时候,则会造成extern "C" { } 的嵌套。这种嵌套是被C++规范允许的。当嵌套发生时,以最内层的嵌套为准。比如在下面代码中,函数foo会使用C++的链接规范,而函数bar则会使用C的链接规范。

如果能够保证一个C语言头文件直接或间接依赖的所有头文件也都是C语言的,那么按照C++语言规范,这种嵌套应该不会有什么问题。但具体到某些编译器的实现,比如MSVC2005,却可能由于 extern "C" { } 的嵌套过深而报告错误。不要因此而责备微软,因为就这个问题而言,这种嵌套是毫无意义的。

你完全可以通过把#include指令放置在extern "C" { }的外面来避免嵌套。拿之前的例子来说,如果我们把各个头文件的 #include 指令都移到extern "C" { } 之外,然后使用C++编译器的预处理选项来编译foo.cpp,就会得到下面的结果:

这样的结果肯定不会引起编译问题的结果——即便是使用MSVC。

把 #include 指令放置在extern "C" { }里面的另外一个重大风险是,你可能会无意中改变一个函数声明的链接规范。比如:有两个头文件a.h,b.h,其中b.h包含a.h,如下:

按照a.h作者的本意,函数foo是一个C++自由函数,其链接规范为"C++"。但在b.h中,由于#include "a.h"被放到了extern "C" { }的内部,函数foo的链接规范被不正确地更改了。

由于每一条 #include 指令后面都隐藏这一个未知的世界,除非你刻意去探索,否则你永远都不知道,当你把一条条#include指令放置于extern "C" { }里面的时候,到底会产生怎样的结果,会带来何种的风险。或许你会说,“我可以去查看这些被包含的头文件,我可以保证它们不会带来麻烦”。但,何必呢?毕竟,我们完全可以不必为不必要的事情买单,不是吗?

Q&A

Q: 难道任何#include指令都不能放在extern "C"里面吗?

A: 正像这个世界的大多数规则一样,总会存在特殊情况。

有时候,你可能利用头文件机制“巧妙”的解决一些问题。比如,#pragma pack的问题。这些头文件和常规的头文件作用是不一样的,它们里面不会放置C的函数声明或者变量定义,链接规范不会对它们的内容产生影响。这种情况下,你可以不必遵守这些规则。

更加一般的原则是,在你明白了这所有的原理之后,只要你明白自己在干什么,那就去做吧。

Q: 你只说了不应该放入extern "C"的,但什么可以放入呢?

A: 链接规范仅仅用于修饰函数和变量,以及函数类型。所以,严格的讲,你只应该把这三种对象放置于extern "C"的内部。

但,你把C语言的其它元素,比如非函数类型定义(结构体,枚举等)放入extern "C"内部,也不会带来任何影响。更不用说宏定义预处理指令了。

所以,如果你更加看重良好组织和管理的习惯,你应该只在必须使用extern "C"声明的地方使用它。即使你比较懒惰,绝大多数情况下,把一个头件自身的所有定义和声明都放置在extern"C"里面也不会有太大的问题。

Q: 如果一个带有函数/变量声明的C头文件里没有extern "C"声明怎么办?

A: 如果你可以判断,这个头文件永远不可能让C++代码来使用,那么就不要管它。

但现实是,大多数情况下,你无法准确的推测未来。你在现在就加上这个extern "C",这花不了你多少成本,但如果你现在没有加,等到将来这个头文件无意中被别人的C++程序包含的时候,别人很可能需要更高的成本来定位错误和修复问题。

Q: 如果我的C+ +程序想包含一个C头文件a . h,它的内容包含了C的函数/变量声明,但它们却没有使用extern "C"链接规范,该怎么办?

A: 在a.h里面加上它。

某些人可能会建议你,如果a.h没有extern "C",而b.cpp包含了a.h,可以在b.cpp里加上 :

extern "C"
{
  #include "a.h"
}

这是一个邪恶的方案,原因在之前我们已经阐述。但值得探讨的是,这种方案这背后却可能隐含着一个假设,即我们不能修改a.h。不能修改的原因可能来自两个方面:

  • 头文件代码属于其它团队或者第三方公司,你没有修改代码的权限;

  • 虽然你拥有修改代码的权限,但由于这个头文件属于遗留系统,冒然修改可能会带来不可预知的问题。

对于第一种情况,不要试图自己进行workaround,因为这会给你带来不必要的麻烦。正确的解决方案是,把它当作一个bug,发送缺陷报告给相应的团队 或第三方公司。如果是自己公司的团队或你已经付费的第三方公司,他们有义务为你进行这样的修改。如果他们不明白这件事情的重要性,告诉他们。如果这些头文 件属于一个免费开源软件,自己进行正确的修改,并发布patch给其开发团队。

在第二种情况下,你需要抛弃掉这种不必要的安全意识。因为,首先,对于大多数头文件而言,这种修改都不是一种复杂的,高风险的修改,一切都在可控的范围之 内;其次,如果某个头文件混乱而复杂,虽然对于遗留系统的哲学应该是:“在它还没有带来麻烦之前不要动它”,但现在麻烦已经来了,逃避不如正视,所以上策 是,将其视作一个可以整理到干净合理状态的良好机会。

Q: 我们代码中关于extern "C"的写法如下,这正确吗?

A: 不确定。

按照C++的规范定义,__cplusplus 的值应该被定义为199711L,这是一个非零的值;尽管某些编译器并没有按照规范来实现,但仍然能够保证__cplusplus的值为非零——至少我到目前为止还没有看到哪款编译器将其实现为0。这种情况下,#if __cplusplus ... #endif完全是冗余的。

但,C++编译器的厂商是如此之多,没有人可以保证某款编译器,或某款编译器的早期版本没有将__cplusplus的值定义为0。但即便如此,只要能够保证宏__cplusplus只在C++编译器中被预先定义 ,那么,仅仅使用#ifdef __cplusplus ⋯ #endif就足以确保意图的正确性;额外的使用#if __cplusplus ... #endif反而是错误的。

只有在这种情况下:即某个厂商的C语言和C++语言编译器都预先定义了__cplusplus ,但通过其值为0和非零来进行区分,使用#if __cplusplus ... #endif才是正确且必要的。

既然现实世界是如此复杂,你就需要明确自己的目标,然后根据目标定义相应的策略。比如:如果你的目标是让你的代码能够使用几款主流的、正确遵守了规范的编译器进行编译,那么你只需要简单的使用#ifdef __cplusplus ... #endif就足够了。

但如果你的产品是一个雄心勃勃的,试图兼容各种编译器的(包括未知的)跨平台产品, 我们可能不得不使用下述方法来应对各种情况 ,其中__ALIEN_C_LINKAGE__是为了标识那些在C和C++编译中都定义了__cplusplus宏的编译器。

这应该可以工作,但在每个头文件中都写这么一大串,不仅有碍观瞻,还会造成一旦策略进行修改,就会到处修改的状况。违反了DRY(Don't Repeat Yourself)原则,你总要为之付出额外的代价。解决它的一个简单方案是,定义一个特定的头文件——比如clinkage.h,在其中增加这样的定义:

以下举例中c的函数声明和定义分别在cfun.h 和 cfun.c 中,函数打印字符串 “this is c fun call”,c++函数声明和定义分别在cppfun.h 和 cppfun.cpp中,函数打印字符串 "this is cpp fun call", 编译环境vc2010.

C++ 调用 C 的方法

c++ 调用 c 的方法,关键是要让c的函数按照c的方式编译,而不是c++的方式。

(1) cfun.h如下:

#ifndef _C_FUN_H_
#define _C_FUN_H_

    void cfun();

#endif 

cppfun.cpp 如下:

//#include "cfun.h"  不需要包含cfun.h
#include "cppfun.h"
#include <iostream>
using namespace std;
extern "C"     void cfun(); //声明为 extern void cfun(); 错误

void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
    return 0;
}

(2)cfun.h同上

cppfun.cpp 如下:

extern "C"
{
    #include "cfun.h"//注意include语句一定要单独占一行;
}
#include "cppfun.h"
#include <iostream>
using namespace std;

void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
    return 0;
}

(3)cfun.h如下:

#ifndef _C_FUN_H_
#define _C_FUN_H_

#ifdef __cplusplus
extern "C"
{
#endif

    void cfun();

#ifdef __cplusplus
}
#endif

#endif

cppfun.cpp如下:

#include "cfun.h"
#include "cppfun.h"
#include <iostream>
using namespace std;

void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
    return 0;
}

C调用 C++ 的方法

C调用C++,关键是C++ 提供一个符合 C 调用惯例的函数。

在vs2010上测试时,没有声明什么extern等,只在在cfun.c中包含cppfun.h,然后调用cppfun()也可以编译运行,在gcc下就编译出错,按照c++/c的标准这种做法应该是错误的。以下方法两种编译器都可以运行。

cppfun.h如下:

#ifndef _CPP_FUN_H_
#define _CPP_FUN_H_

extern "C" void cppfun();


#endif

cfun.c如下:

//#include "cppfun.h" //不要包含头文件,否则编译出错
#include "cfun.h"
#include <stdio.h>

void cfun()
{
    printf("this is c fun call\n");
}

extern void cppfun();

int main()
{
#ifdef __cplusplus
    cfun();
#endif
    cppfun();
    return 0;
}

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

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

相关文章

CSDN·COC城市开发者组织2023年度聚会上海站纪实

目录 前言活动宣传活动议程活动总结结束语 前言 2023年是CSDNCOC成立之年&#xff0c;经过一年的不断发展和壮大&#xff0c;COC上海城市开发者社区的队伍不断壮大&#xff0c;本着每个月都有线下活动的原则&#xff0c;先后举办大大小小的线下沙龙活动20余场。与此同时&…

16. 输入设备应用编程

16. 输入设备应用编程 1. 输入类设备编程介绍1.1 什么是输入设备1.2 input 子系统1.3 读取数据的流程1.4 应用程序如何解析数据 2. 读取 struct input_event 数据3. 按键应用编程4. 触摸屏应用编程4.1 解析触摸屏设备上报的数据4.1.1 单点触摸设备——事件上报顺序4.1.2 多点触…

ARL灯塔vps云服务器安装

前提是vps服务器已经安装好docker 1、下载压缩包到本地 2、解除不能扫描edu等域名的限制 解压docker_arl.zip&#xff0c;打开docker_arl/config-docker.yaml文件 删除掉edu.cn等限制域名(图中已删除) 3、上传至vps云服务器 将docker_arl文件上传到云服务器opt目录下 这里我…

【开源之美】:cppcheck

一、项目链接 https://github.com/danmar/cppcheck/tree/main 二、效果示例

STM32 E18-D80NK红外避障传感器

E18-D80NK-N是一款红外光电传感器&#xff0c;它同时具备发射和接收功能。通过对发射光进行调制后发出&#xff0c;并通过接收头对反射光进行解调输出。 E18-D80NK-N采用了透镜来增强传感器的性能&#xff0c;使其能够检测更远的距离。根据红外光的特性&#xff0c;不同颜色的…

如何将前后端分离(vue2+SpringBoot)项目部署到腾讯云服务器

如何将前后端分离&#xff08;vue2SpringBoot&#xff09;项目部署到腾讯云服务器 目录 如何将前后端分离&#xff08;vue2SpringBoot&#xff09;项目部署到腾讯云服务器 1、在选中目录地下新建2个文件夹 2、将打包好的前端项目和后端jar包上传到相应的目录下 3、将路径切…

基于Prompt Learning的信息抽取

PTR: Prompt Tuning with Rules for Text Classification 清华&#xff1b;liuzhiyuan&#xff1b;通过规则制定subpromptRelation Extraction as Open-book Examination: Retrieval-enhanced Prompt Tuning Relation Extraction as Open-book Examination: Retrieval-enhance…

R语言【taxlist】——print_name():为发表文章准备可用的格式化名称

Package taxlist version 0.2.4 Description 在撰写关于生物多样性的文章时&#xff0c;可以使用名称自动插入到文件中&#xff0c;包括学名不同元素的典型斜体格式。函数print_name既可以应用于 Markdown 文档&#xff0c;也可以应用于图形。在 Rmarkdown 文档中&#xff0c;…

解决方案—幻兽帕鲁Palworld私服部署 一杯茶的功夫搭建部署一个属于自己的游戏私服

《幻兽帕鲁》是Pocketpair开发的一款开放世界生存制作游戏 &#xff0c;游戏于2024年1月18日发行抢先体验版本&#xff0c;游戏中&#xff0c;玩家可以在广阔的世界中收集神奇的生物“帕鲁”&#xff0c;派他们进行战斗、建造、做农活&#xff0c;工业生产&#xff0c;游戏目前…

重磅福利!23篇AI-Agents智能代理必读论文!不容错过!

会议之眼 快讯 在数字化时代&#xff0c;AI已经成为各行各业的驱动力&#xff0c;推动科技和社会快速发展。AI-Agents被称为“AI代理”或“智能代理”&#xff0c;是一项前沿应用。它们具备自主感知、决策和执行任务的能力。通过分析传感器信息或用户指令&#xff0c;AI-Agents…

一键去除视频水印,让你的视频更完美

你是否曾经因为视频中的水印而烦恼&#xff1f;现在&#xff0c;有了我们的视频剪辑工具&#xff0c;你可以轻松去除视频中的水印&#xff0c;让你的视频更加完美&#xff01;这款工具采用先进的视频处理技术&#xff0c;能够一键批量去除水印&#xff0c;让你的视频更加干净整…

【JavaScript基础入门】03 JavaScript 基础语法(一)

JavaScript 基础语法&#xff08;一&#xff09; 目录 JavaScript 基础语法&#xff08;一&#xff09;1. JS 初体验2. JavaScript注释2.1 单行注释2.2 多行注释 3. JavaScript结束符4. JavaScript输入输出语句 1. JS 初体验 JS 有3种书写位置&#xff0c;分别为内联、内部和外…

TBTOOLS标记染色体上的gene

最近碰到个问题&#xff0c;如何在染色体上标记gene, 查了下发现了个windows工具&#xff1a;TBTOOLs &#xff0c; 有点意思&#xff0c;真是简洁高效。 话不多说&#xff0c;直接干。。 工具下载 直接在github 下载即可 https://github.com/CJ-Chen/TBtools-II&#xff0c;…

【Java程序设计】【C00177】基于SSM的IT论坛管理系统(论文+PPT)

基于SSM的IT论坛管理系统&#xff08;论文PPT&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于ssm的IT论坛管理系统 本系统分为前台系统、后台管理员、后台教师以及后台用户4个功能模块。 前台系统&#xff1a;当游客打开系统的网址后&#xff0…

JavaScript定义变量及赋值

定义变量及赋值 ☞ 定义变量,未赋值var 变量名; 默认值是undefined ☞ 定义变量,且赋值var 变量名 数据;☞ 总结:1. 一个变量一次只能保存一个值&#xff1b;2. 以最后一次赋值为准3. JS变量区分大小写变量命名规范 ☞ 规则 必须遵守的&#xff0c;不遵守的话 JS引擎 发…

平安银行广州分行:用有温度的金融服务,实现健康与财富双重守护

甄选优质权益&#xff0c;守护客户身心健康   2024年1月&#xff0c;平安银行广州分行成功举办两场心理疗愈工作坊活动&#xff0c;旨在助力客户们摆脱工作和生活的喧嚣繁忙&#xff0c;在平静的疗愈气氛中拥抱自我、感受内心的平和。在“律动心弦-音乐疗愈工作坊”活动中&am…

C语言王道第八周一题

Description 初始化顺序表&#xff08;顺序表中元素为整型&#xff09;&#xff0c;里边的元素是 1,2,3&#xff0c;然后通过 scanf 读取一个元素&#xff08;假如插入的是 6&#xff09;&#xff0c;插入到第 2 个位置&#xff0c;打印输出顺序表&#xff0c;每个 元素占 3 个…

堆和堆排序【数据结构】

目录 一、堆1. 堆的存储定义2. 初始化堆3. 销毁堆4. 堆的插入向上调整算法 5. 堆的删除向下调整算法 6. 获取堆顶数据7. 获取堆的数据个数8. 堆的判空 二、Gif演示三、 堆排序1. 堆排序(1) 建大堆(2) 排序 2.Topk问题 四、完整代码1.堆的代码Heap.cHeap.htest.c 2. 堆排序的代码…

RT-Thread: STM32 SPI使用流程

1.添加驱动 ①点开设置界面 ②勾选看门 SPI 驱动 ③点击保存 ④查看添加的驱动文件 drv_spi.c 2.打开驱动头文件定义 ①打开配置文件 ②打开定义 3.打开需要开启的SPI总线 打开 drivers 目录下的 board.h 用SPI搜索&#xff0c;找到如下文字&#xff0c;打开对应的宏。 /*-…

Windows 和 Anolis 通过 Docker 安装 Milvus 2.3.4

Windows 10 通过 Docker 安装 Milvus 2.3.4 一.Windows 安装 Docker二.Milvus 下载1.下载2.安装1.Windows 下安装&#xff08;指定好Docker文件目录&#xff09;2.Anolis下安装 三.数据库访问1.ATTU 客户端下载 一.Windows 安装 Docker Docker 下载 双击安装即可&#xff0c;安…