首页 > 编程语言 >3.C++和C的混合编译

3.C++和C的混合编译

时间:2022-12-04 19:58:19浏览次数:66  
标签:调用 函数 C++ 混合 编译 extern 链接

简介

C++ 语言的创建初衷是 "a better C",但是这并不意味着 C++ 中类似 C 语言的全局变量和函数所采用的编译和连接方式与 C 语言完全相同。作为一种欲与 C 兼容的语言, C++ 保留了一部分过程式语言的特点(被世人称为"不彻底地面向对象"),因而它可以定义不属于任何类的全局变量和函数。但是, C++ 毕竟是一种面向对象的程序设计语言,为了支持函数的重载, C++ 对全局函数的处理方式与 C 有明显的不同。

本文将介绍如何通过 extern "C" 关键字在 C++ 中支持 C 语言 和 在C语言中如何支持 C++

某企业曾经给出如下的一道面试题

为什么标准头文件都有类似以下的结构?

//head.h
#ifndef HEAD_H
#define HEAD_H

#ifdef __cplusplus
extern "C" {
#endif

    /*...*/

#ifdef __cplusplus
}
#endif

#endif /* HEAd_H */

问题分析

  • 这个头文件head.h可能在项目中被多个源文件包含(#include "head.h"),而对于一个大型项目来说,这些冗余可能导致错误,因为一个头文件包含类定义或inline函数,在一个源文件中head.h可能会被#include两次(如,a.h头文件包含了head.h,而在b.c文件中#include a.h和head.h)——这就会出错(在同一个源文件中一个结构体、类等被定义了两次)。
  • 从逻辑观点和减少编译时间上,都要求去除这些冗余。然而让程序员去分析和去掉这些冗余,不仅枯燥且不太实际,最重要的是有时候又需要这种冗余来保证各个模块的独立

为了解决这个问题,上面代码中的

#ifndef HEAD_H
#define  HEAD_H
/*……………………………*/
#endif /* HEAD_H */

就起作用了。如果定义了HEAD_H,#ifndef/#endif之间的内容就被忽略掉。因此,编译时第一次看到head.h头文件,它的内容会被读取且给定HEAD_H一个值。之后再次看到head.h头文件时,HEAD_H就已经定义了,head.h的内容就不会再次被读取了。

那么下面这段代码的作用又是什么呢?

#ifdef __cplusplus
extern "C" {
#endif
/*.......*/
#ifdef __cplusplus
}
#endif

我们将在后面对此进行详细说明。

关于 extern "C"

前面的题目中的 __cplusplus 宏,这是C++中已经定义的宏,是用来识别编译器的,也就是说,将当前代码编译的时候,是否将代码作为 C++ 进行编译。

首先从字面上分析extern "C",它由两部分组成:extern关键字、"C"。下面我就从这两个方面来解读extern "C"的含义。

首先,被它修饰的目标是 extern 的;其次,被它修饰的目标是 C 的。

extern关键字

被 extern "C" 限定的函数或变量是 extern 类型的。

extern是C/C++语言中表明函数全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。

被extern修饰的函数,需要在编译阶段去链接该目标文件,并且与extern对应的关键字是 static,被static修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其一般是不可能被extern “C”修饰的。

注意:例如语句 extern int a; 仅仅是对变量的声明,其并不是在定义变量 a ,声明变量并未为 a 分配内存空间。定义语句形式为 int a; 变量 a 在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。

被 extern "C" 修饰的变量和函数是按照 C 语言方式编译和连接的。

由于C++和C两种语言的亲密性,并且早期大量的库都是由C语言实现的,所以不可避免的会出现在C++程序中调用C的代码、C的程序中调用C++的代码,但是它们各自的编译和链接的规则是不同的。

函数名修饰

  1. 由于Windows下vs的修饰规则过于复杂,而Linux下gcc的修饰规则简单易懂,下面我们使用了gcc演示了这个修饰后的名字。
  2. 通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。

分别使用C的编译器和C++的编译器去编译并获得一个可执行文件

  • 使用C语言(gcc)编译器编译后结果

使用objdump -S 命令查看gcc生成的可执行文件:

image-20220518225738183

  • 使用C++编译器(g++)编译后结果

使用objdump -S 命令查看g++生成的可执行文件:

image-20220518225745296

linux:修饰后的函数名= _Z + 函数名长度 + 形参类型首字母,Windows下也是相似的,细节上会有所不同,本质上都是通过函数参数信息去修饰函数名。

C++的编译和链接方式

采用g++编译完成后,函数的名字将会被修饰,编译器将函数的参数类型信息添加到修改后的名字中,因此当相同函数名的函数拥有不用类型的参数时,在g++编译器看来是不同的函数,而我们另一个模块中想要调用这些函数也就必须使用C++的函数名修饰规则去链接函数(找修饰后的函数名)才能找到函数的地址。

C的编译和链接方式

对于C程序,由于不支持重载,编译时函数是未加任何修饰的,而且链接时也是去寻找未经修饰的函数名。

C和C++直接混合编译时的链接错误

在C++程序,函数名是会被参数类型信息修饰的,这就造成了它们之间无法直接相互调用。

例如:

print(int)函数,使用g++编译时函数名会被修饰为 _Z5printi,而使用gcc编译时函数名则仍然是print,如果直接在C++中调用使用C编译规则的函数,会链接错误,因为它会去寻找 _Z5printi而不是 print。

【C和C++的编译和链接方式的不同】参考:

C++的函数重载 - 吴秦 - 博客园

extern“C”的使用

extern "C"指令非常有用,因为C和C++的近亲关系。注意:extern "C"指令中的C,表示的一种编译和连接规约,而不是一种语言。

并且extern "C"指令仅指定编译和连接规约,并不影响语义,编译时仍是一个C++的程序,遵循C++的类型检查等规则。

对于下面的代码它们之间是有区别的

extern "C" void Add(int a, int b);
//指定Add函数应该根据C的编译和连接规约来链接
extern void Add(int a, int b);
//声明在Add是外部函数,链接的时候去调用Add函数

如果有很多内容要被加上extern “C”,你可以将它们放入extern “C”{ }中。

通过上面的分析,我们知道extern "C"的真实目的是实现类C和C++的混合编程,在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等。

那么混合编译首先要处理的问题就是要让我们所写的C++程序和C程序函数的编译时的修饰规则链接时的修饰规则保持一致。

总共就有下面四种情况,也就是说一个C的库,应该能同时被C和C++调用,而一个C++的库也应能够同时兼容C和C++。

image-20220518225753095

为了展示如上四种情况,我们分别建立一个C静态库和C++静态库

C程序能调用C的库,C++程序能调用C++的库,这是理所应当的,因此我们关注的问题是如何交叉调用

用法举例

静态库是什么

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常,之所以称为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。

试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:

  • 静态库对函数库的链接是放在编译时期完成的。
  • 程序在运行时与函数库再无瓜葛,移植方便。
  • 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库,因此体积较大

创建C静态库

我们以一个栈的静态库为例:

  • 首先新建项目Stack_C

image-20220518225759900

  • 新建源文件和头文件

image-20220518225807539

  • 写好栈的代码

注意一定是C程序,即源文件后缀为c

image-20220518225814328

  • 更改输出文件类型

右键项目名称—>属性

image-20220518225821196

  • 更改为配置类型为静态库

image-20220518225828078

  • 生成静态库

image-20220518225835992

  • 查看是否生成成功

VS一般在项目路径下的x64\Debug路径下:

image-20220518225845182

至此,静态库已经可以成功建立了。

  • 再新建一个项目,写一个去调用该静态库实现的栈的程序(以括号匹配问题为例)

不过对于VS我们的静态库是默认不去使用的,因此我们需要将静态库的路径和库的名称分别添加到库目录和依赖项,才能让程序能去调用该静态库。

image-20220518225851175

  • 更改链接器配置

右键项目名—>点击属性

“属性面板“—>”配置属性”—> “链接器”—>”常规”,附加依赖库目录中输入,静态库所在目录;

增加库目录(路径为我们刚刚生成的静态库所在的Debug文件夹)

image-20220518225858234

  • 增加附加依赖项

名称为Stack_C项目生成的静态库名,一般是项目名 + .lib

“属性面板”—>”配置属性”—> “链接器”—>”输入”,附加依赖库中输入静态库名StaticLibrary.lib。

image-20220518225905839

我们先尝试使用C程序来调用该静态库

新建项目

  1. 将源文件后缀改为c;
  2. 包含上Stack_C项目(静态库项目)的头文件;
  3. 点击生成解决方案;

image-20220518225913686

成功生成,说明成功调用。

尝试使用C++程序调用C静态库

  1. 将源文件后缀改为cpp;
  2. 头文件保持不变;
  3. 点击生成解决方法;

结果报错了:

image-20220518225924430

这说明在链接的过程中出现了问题,也就是在我们的程序找不到静态库中函数的地址,原因是我们的静态库是C语言的,没有对函数进行修饰,但在我们的调用方是C++程序,在链接过程中找的是修饰过的函数名,因此无法找到函数的地址。

既然C语言的静态库只能按照C的规则去编译这些函数(即不修饰函数名),那么我们只要让C++程序按照C语言的链接规则(即找未经修饰的函数名)去找到函数不就解决了?

首先可以确定的是,C的库所遵守的规则必然是C的,那么C程序来调用该库是没问题的,但是如果是C++调用该库就会由于双方遵守的规则的不同而链接错误。

解决的两种思路:

  1. 改变C库的编译和链接方式为C++规则;
  2. 改变C++程序调用库函数的编译和链接方式为C的规则;

方法1是不行的,因为C语言中可没有extern “C++”这种东西,那么考虑方法2;

这时我们可以借助extern“C”改变C++程序的链接规则,让C++去按照C的规则去找函数名,即未经过任何修饰的函数名,那就一定能找到函数的地址,来去正确调用静态库。

在源文件test.cpp使用extern “C”,去改变包含的头文件中的函数的链接规则。

//调用库的的模块的头文件包含
extern "C"
{
	#include"..\..\Stack_C\Stack_C\stack.h"
}
//程序的代码
//...

那么在test.cpp去链接这些库函数时,就会直接去找未被修饰的原函数名。

这样就解决了。

还有一个一步到位的解决方法,利用条件编译,根据当前程序的类型,选择是否去执行extern “C”指令。

  • 调用方是C程序,不做处理;
  • 调用方是C++程序,需要使用extern“C”将程序改为C的链接规则;
//调用库的的模块的头文件包含
#ifdef __cplusplus//如果是c++程序,就执行extern “C”,使用C的链接方式,去找未经修饰的函数名
extern "C"{
#endif
#include"..\..\Stack_C\Stack_C\stack.h"
#ifdef __cplusplus
}
#endif
//程序的代码
//...

但是这样的处理不太好,我们作为调用方自然是想可以直接通过头文件包含的方式就能使用库里的函数,因此采用下列方法,更改库的头文件函数声明为:

#ifdef __cplusplus//如果定义了宏__cplusplus就执行#ifdef 到 #endif之间的语句
extern "C"
{
#endif
void StackInit(struct Stack* s);
void StackPush(struct Stack* s, DataType x);
void StackPop(struct Stack* s);
DataType StackTop(struct Stack* s);
int StackSize(struct Stack* s);
void StackDestory(struct Stack* s);
bool StackEmpty(struct Stack* s);
#ifdef __cplusplus
}
#endif

库的规则

库是C的静态库,条件编译会忽略extern“C”,并且它的编译和链接规则是无法改变的,只能是C的规则。

调用方的规则

  1. 若是C程序去调用,条件编译忽略掉extern“C”,程序不会报错,并且C程序所遵守的规则与库的规则是一致的,可以正常调用;
  2. 若是C++程序去调用,条件编译extern“C”就会生效,使得调用方去使用这几个库函数时的规则不再遵守C++的规则而是C的规则,从而可以正常调用;

这样的一段代码,无论是C++程序还是C程序都可以直接#include头文件路径就能去调用该静态库了。

创建C++静态库

步骤和创建C的静态库相同,只不过要将项目中的源文件后缀改为cpp,就会生成一个C++的静态库,因此不再阐述。

创建完成后,我们仍使用刚刚的项目,并且添加C++静态库路径到库目录,添加C++静态库名称到附加依赖项,仍然以括号匹配问题为例去调用该库。(记得删除C静态库的库目录和附加依赖项,否则我们的程序有可能还会去调用C的静态库,这样我们就无法探究如何去调用C++静态库的问题了)

尝试使用C程序调用C++静态库

我们不着急调用,经过先前的经验,这里可以判断,C++的程序去调用C++的库一定是没问题的,但是C程序就不好说了,因此我们要搞定C程序调用C++库的情况,先搞清楚它们的差异:

首先C++的库若不经任何处理,那么它编译链接规则一定是遵守C++的规范的,但是这样的话C程序调用它,无论如何也无法正常调用的。

那么换种思路,使用extern“C”让C++的库的规则改为C的规则,那么这样C程序是一定可以调用该C++的库的,而C++程序则会因为双方所遵守的规则不同而链接错误。

这样的话相当于C++库被改为了C的库,仍然使用C++程序调用C的库的解决方案:

  • 改变C++程序调用库函数的编译和链接方式为C的规则;

如果对库的头文件中的函数做如下处理:

//用C的规则去搞 库的编译和链接方式
extern "C"
{
	void StackInit(struct Stack* s);
	void StackPush(struct Stack* s, DataType x);
	void StackPop(struct Stack* s);
	DataType StackTop(struct Stack* s);
	int StackSize(struct Stack* s);
	void StackDestory(struct Stack* s);
	bool StackEmpty(struct Stack* s);
}

那么现在C++的静态库的函数名都是没有经过修饰的。(C的规则)

但是我们去编译仍然报错:

error C2059: 语法错误:“字符串”

"StackInit”未定义;假设外部返回int

“StackPush”未定义;假设外部返回int

“StackEmpty”未定义;假设外部返回int

“StackTop”未定义;假设外部返回int

“StackPop”未定义;假设外部返回int

这是因为我们使用C程序时也包含了此头文件,头文件展开后C语言中无法识别extern“C”,因此报错。

我们尝试使用条件编译来决定是否使用extern“C”,根据调用方的不同改变函数链接规则:

  • 调用方是C++程序,那么需要使用extern“C”将C++程序的函数链接规则变为C的;
  • 调用方是C程序,不使用extern“C”语句做处理;

因此我们做如下处理,将库的头文件中的函数声明加上:

#ifdef __cplusplus//如果定义了宏__cplusplus就执行#ifdef 到 #endif之间的语句
extern "C"
{
#endif
void StackInit(struct Stack* s);
void StackPush(struct Stack* s, DataType x);
void StackPop(struct Stack* s);
DataType StackTop(struct Stack* s);
int StackSize(struct Stack* s);
void StackDestory(struct Stack* s);
bool StackEmpty(struct Stack* s);
#ifdef __cplusplus
}
#endif

库的规则

如此一来,C++静态库的上面这些函数,都是遵守C语言的编译和链接规则的。

调用方的规则

  1. 如果是C的程序来调用,调用方的extern“C”被条件编译忽略,库和调用方的规则是一致的,可以正常调用;
  2. 如果是C++的程序来调用,那么调用方的extern “C”就会发挥作用,让调用方也是遵守C的规则,与库的规则一致,就可以正常调用了;

总结:C++和C之间的混合编译,为了消除函数名修饰规则不同的的差别,我们需要使用extern ”C“来改变C++的编译和连接方式。

但这样问题也随之而来:

被extern“C”的C++的库函数就失去了函数重载的特性,如果库的这些函数中有同名函数,那么就无法正确编译,因为按照C的方式去编译,函数名会冲突。

如何解决这个问题呢?

实际上这个问题无法解决,一旦选择了将某个函数指定了按照C的方式去编译链接,那么这个函数就已经失去了重载的特性了,不过Cpp的库中未被指定按照C的规则去编译和链接的那些函数,仍然可以被重载,并且具有C++的一切特性。

因此这个问题无解,只有通过避免“一刀切”的方法来保护那些我们想重载的函数,也就是说一部分库里的函数是实现给C程序调用的,我们就通过extern“C”改变它的编译和链接方式,而对于那些实现给C++程序调用的函数接口,我们不做任何处理,并且不暴露给C程序。

想要实现上述过程,我们需要在静态库项目中创建两个头文件libc.hlibcpp.hlibc.h声明那些需要暴露给C程序的函数接口,并且使用上面介绍的条件编译和extern“C”,libcpp.h声明那些暴露给给Cpp程序的函数接口,这样两个头文件的函数的链接规范互不相同,也互不干扰。只需要将lic.h在C程序调用的地方使用#include 包含,libcpp.h在C++程序调用的地方使用#include包含即可使用。

因此C++库中哪个接口需要暴露给C,我们就用extern“C”修饰哪个接口。

image-20220518225938546

总之,C的库可以给C程序和C++程序调用,而C++库也可以被C程序和C++程序调用

如果要满足这个库中所有的函数都能同时被C++和C调用,那么无论是C的库还是C++的库,最终这个库的编译和链接方式都只能是C的规范,因为C++可以使用C的链接规范但是C不能使用C++的链接规范,也就导致了如果库的链接规范是C++的,那么无论如何,C程序都无法调用。

值得一提的是C++程序中的函数可以使用两种链接规范,因此我们可以针对函数的使用场景来选择该函数的编译和链接规范,使得一部分函数保留C++的特性,但一部分函数就只能为了兼容C而牺牲C++的特性,想要既兼容C又保留C++的特性,这是做不到的。

标签:调用,函数,C++,混合,编译,extern,链接
From: https://www.cnblogs.com/ncphoton/p/16950512.html

相关文章

  • 2.C++入门基础(下)
    内联函数C++中函数的使用我们已经比较清楚了,与C语言中函数的使用大多相同,主要是增加了重载的特性,对C语言的函数的一些缺陷做了一些补充。那么对于一些比较简单却又经常使......
  • 7.C++拷贝构造函数
    拷贝构造函数我们经常会用一个变量去初始化一个同类型的变量,那么对于自定义的类型也应该有类似的操作,那么创建对象时如何使用一个已经存在的对象去创建另一个与之相同的对......
  • 6.C++构造函数
    类的6个默认成员函数如果我们写了一个类,这个类我们只写了成员变量没有定义成员函数,那么这个类中就没有函数了吗?并不是的,在我们定义类时即使我们没有写任何成员函数,编译器......
  • 5.C++类和对象(上)
    面向过程和面向对象初步认识C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是基于面向对象的,关注的是对象,将一件事拆分成不同的对象......
  • 11.C++日期类的实现
    日期类的实现在前面学过默认成员函数后,我们就可以写一个简单的日期类了。如何写呢?我们可以先分析分析。日期类的成员变量都是int类型,那么构造函数是要显式定义的,成员变......
  • 10.C++类和对象(下)
    再谈构造函数之前讲过构造函数的一些特性,再在这里补充下。构造函数体赋值classDate{public: Date(intyear,intmonth,intday) { _year=year; _month=m......
  • 9.C++运算符重载
    运算符重载本文包括了对C++类的6个默认成员函数中的赋值运算符重载和取地址和const对象取地址操作符的重载。运算符是程序中最最常见的操作,例如对于内置类型的赋值我们直......
  • 8.C++析构函数
    析构函数既然在创建对象时有构造函数(给成员初始化),那么在销毁对象时应该还有一个清除成员变量数据的操作咯。概念析构函数:与构造函数功能相反,析构函数不是完成对象的销......
  • 13.C++模板初阶
    泛型编程如何实现一个通用的交换函数呢?voidSwap(int&left,int&right){ inttemp=left; left=right; right=temp;}voidSwap(double&left,double&ri......
  • 12.C++内存管理
    在C语言的学习中我们已经接触过内存管理,那么C++与C语言之间又有什么不同和相同的地方呢?C++内存分布intglobalVar=1;staticintstaticGlobalVar=1;voidTest(){......