C++ 入门基础(二) 详解函数重载/项目链接静态库

1.函数重载是什么

函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题

1.1样例

#include <iostream>
using namespace std;

int Add(int a, int b)
{
	return a + b;
}
double Add(double a, double b)
{
	return a + b;
}
long Add(long a, long b)
{
	return a + b;
}

int main()
{
	cout << Add(10, 20) << endl;
	cout << Add(10.5, 20.0) << endl;
	cout << Add(10L, 20L) << endl;
}

运行结果如下👇


①问:如果在main函数中再加一句cout << Add(10.5, 20) << endl编译器会怎么样呢?
答:会对其进行报错,原因是传的参数一个为double,另一个为int,而这种函数并没有被定义。

②问:下面的这两个函数算函数重载吗?

short Add(short left, short right)
{
 return left+right;
}
int Add(short left, short right)
{
 return left+right;
}

答:并不属于,由于同名函数的参数必须在参数个数、类型或数据上有所不同。而此时所有参数的相同,故不构成函数重载,编译器会进行报错。


2. 函数调用链接详解

为什么该语法在C++中是支持的,而在C语言中是不支持的呢?下面我们基于Linux系统进行讲解,创建下面三个文件,这里我们先讲解编译器对目标文件进行链接时,具体做了什么事👇

在这里插入图片描述

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接 详解戳这里👉程序的编译(翻译环境详解)

了解过程序的编译,我们就能知道:汇编时test.cppf.cpp会形成它们对应的符号表来存储函数对应的地址(下面的地址是虚构参考,并非实际地址)
对于只声明,未定义的函数会对其地址进行默认的填充,如下图👇
在这里插入图片描述
在链接的时候,将符号表合并后才能找到真正的地址👇
在这里插入图片描述

如果只声明未定义的话,编译器在编译和汇编都能正常执行,但是在链接的时候就会发现调用的函数没有实际地址,随之报链接错误,从而终止程序运行👇


我们此时打开Visual Studio 2019编译器中转到汇编代码验证上面的情形

当我们运行到调用函数的时候,一定会call一个jmp指令的地址,再通过jmp指令跳转到f()函数真正的地址,如下图👇

在这里插入图片描述
我们可以这么理解:函数的声明就相当于是你向编译器承诺有这个函数,编译器就可以正常编译、汇编。
而将承诺兑现是链接的时候做的事情,这个过程中,编译器将指令合并起来👇
在这里插入图片描述
因此如果无法在test.o的符号表中找到该函数地址的话,程序报的是链接错误。


3.名字修饰

在了解了函数调用时链接所扮演的角色后,接下来我们就能更好地理解为什么函数调用在C语言中是不支持的,而在C++中是可以支持的。

链接时我们需要通过函数名,在合并的符号表中,寻找被调用函数的实际地址。
由于C语言的函数名直接用的就是原来的名字,在编译阶段进行符号汇总时出现两个名字相同的函数就出现了冲突,因此在编译阶段就会报错。
而在C++中,会对相同的函数名称进行名字修饰(name Mangling),从而在调用函数时找到对应的函数地址。
接下来便基于Linux环境下进行演示👇

3.1 C++汇编指令中的函数命名

输入指令使f.cpptest.cpp生成可执行文件tcpp👇

g++ -o tcpp f.cpp test.cpp

在这里插入图片描述
输入指令观察C++汇编指令中的两个函数的名称修饰规则👇

objdump -S tcpp

如上图所示第一个函数的名称为 _Z1fid ,第二个函数的名称为 _Z1fdi

汇编代码中名字修饰规则:_Z+函数名长度+函数名+类型首字母👇

  • 最前面的_Z是由编译器定的前缀,并不重要,具体原因这里不做解释。
  • 1是代表函数名的长度为1
  • 上图中最后两个字母 id / di 代表的是参数类型的首字母,也就是说C++中参数类型的首字母带进命名规则中去了。因此参数的类型、个数、顺序不同,它们的函数名字都不一样,这就是C++函数重载区分两个名称相同函数的原因。

了解了上面的规则,我们便可以以此类推:如果我们定义了一个函数void func(int* a, char ch)
那么它在汇编代码中的名字修饰为:_Z4funcPic(Pi为int*类型指针,c为类型char)。

3.2 C语言汇编指令中的函数命名

输入指令使f.ctest.c生成可执行文件tc👇

gcc -o tc f.c test.c

在这里插入图片描述
输入指令观察C++汇编指令中的两个函数的名称修饰规则👇

objdump -S tc

在这里插入图片描述
如上图所示我们发现对应的汇编指令中的函数名称就是以原名称进行命名的。

3.3 总结

  1. 在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
  2. 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数名字修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
  3. 另外我们也理解了,为什么函数重载要求参数不同,而跟返回值没关系。

4.链接静态库/动态库

理解了上面这些知识,我们就可以自行配置静态库进行使用了。对于静态库/动态库具体是做什么的,暂时不需要了解。这里的重点是如何配置一个静态库。

4.1 C++调用C静态库

C++一定是可以调用C++的静态库,那它要如何调用C的静态库呢?
下面用于配置静态库的代码使用的是之前博主写过的“Stack(栈)”的数据结构,对于代码具体写了什么可以不用在意,重要的是配置静态库的过程,以及如何使用C++代码调用C的静态库。

在这里插入图片描述

  • 配置静态库

PS:这里一定要生成一下


  • 此时就成功生成了一个静态库Stack_C.lib了👇
    在这里插入图片描述

  • 随便拷一个需要调用静态库的代码到另外一个项目test_05_08项目中👇

  • 包含静态库的头文件

首先我们要知道头文件的相对于newtest.cpp的位置在哪👇
在这里插入图片描述

因此,包含静态库我们写下这样的代码👇

#include "../../Stack_C/Stack_C/Stack.h"

如果是VS 2019还需要附加包含目录,否则可能会出现“无法打开源文件”的问题
我们需要确保外部依赖项中有Stack.h文件👇

下面为附加包含目录的操作👇

在这里插入图片描述
上述操作后就成功包含头文件了,此时程序不再出现语法错误。

  • 生成对应的DEBUG
    在这里插入图片描述
  • 附加依赖项
    在这里插入图片描述接着就可以点击应用了

给大家捋一捋上面的操作,首先我们将Stack_C的头文件Stack.h包含进来,也就是有相关函数声明,编译器就不会再报错了。
但此时还链接不上这些函数具体的函数地址,于是在链接器中配置,使它链接上Stack_C.lib。此时就可以在我们配置的库里面的符号表中找到函数的具体地址

4.1.1 extern "C"的使用

此时运行程序,还是会报链接错误👇这是为什么呢?
在这里插入图片描述
由前面所提到的C++的名字修饰规则,我们了解了C++的汇编代码中名字修饰规则是:_Z+函数名长度+函数名+类型首字母,而C的汇编代码中无名字修饰,也就是说C的汇编代码中函数名为原函数的函数名。
此时test_05_08文件是.cpp文件,而Stack_C中是.c文件。因此会出现链接时通过函数名无法找到相应的函数地址,随之产生链接错误。
我们知道C++是兼容C的库的,因此要想在.cpp文件中调用C静态库,其实很简单👇

extern "C"
{
    #include "../../Stack_C/Stack_C/Stack.h"
}

也就是说Stack.h中的函数,在extern "C"{ }中展开了。它的作用是:告诉编译器,extern "C"声名的函数是C库,要用C的方式去形成符号表,此时程序就可以成功运行👇

4.2 C调用C++静态库

  • 准备工作

配置静态库的步骤和上面一样,这里就不过多解释了

此时运行程序出现链接错误 同样的道理,这是由于名字修饰所导致的结果。

问:前面的 extern "C"的使用这么方便,那这里可以用extern "CPP"吗?
答:很遗憾,C在汇编代码中的函数命名方式是原函数名,编译器是无法通过这种操作找到修饰过的函数名的。
举个例子:C++中声明的某一函数f(int a, double b);,在符号表中命名为Z1fid,要想在C的库中寻找定义的函数,完全可以以C的命名f形成符号表。
但是反过来,C中声明的函数f(int a, double b);符号表中命名为f,C无法做到以C++的命名方式Z1fid形成符号表。

4.2.1 第一种处理方法

对cpp库中的文件下手,使其以C的方式形成符号表👇

newtest.c进行编译,此时出现的是语法错误,原因是:由于#include 包含头文件,在预编译时会将其展开,而C中并没有extern "C"这样的语法,因此会在编译阶段报语法错误👇

处理方法👇
通过预处理指令和宏的处理下:
如果是.cpp文件,就将extern "C" 放出来代替EXTERN_C,否则用空白代替EXTERN_C

此时问题就解决了

4.2.2 第二种处理方法

如果库函数很多,采用上面的方式就显得过于复杂,接下来是第二种处理方法👇

#ifdef __cplusplus
extern "C"
{
#endif

	void StackInit(ST* ps);
	void StackDestory(ST* ps);
	void StackPush(ST* ps, STDataType x);
	void StackPop(ST* ps);
	bool StackEmpty(ST* ps);
	int StackSize(ST* ps);
	STDataType StackTop(ST* ps);

#ifdef __cplusplus
}
#endif
  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

干脆面la

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值