函数重载/项目链接静态库
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.cpp
和f.cpp
会形成它们对应的符号表来存储函数对应的地址(下面的地址是虚构参考,并非实际地址)
对于只声明,未定义的函数会对其地址进行默认的填充,如下图👇
在链接的时候,将符号表合并后才能找到真正的地址👇
如果只声明未定义的话,编译器在编译和汇编都能正常执行,但是在链接的时候就会发现调用的函数没有实际地址,随之报链接错误,从而终止程序运行👇
![](https://img-blog.csdnimg.cn/b99e3525f7674ed1bdf5824d8f4cea2b.gif#pic_center)
我们此时打开Visual Studio 2019
编译器中转到汇编代码验证上面的情形
当我们运行到调用函数的时候,一定会call
一个jmp
指令的地址,再通过jmp指令跳转到f()
函数真正的地址,如下图👇
我们可以这么理解:函数的声明就相当于是你向编译器承诺有这个函数,编译器就可以正常编译、汇编。
而将承诺兑现是链接的时候做的事情,这个过程中,编译器将指令合并起来👇
因此如果无法在test.o的符号表中找到该函数地址的话,程序报的是链接错误。
3.名字修饰
在了解了函数调用时链接所扮演的角色后,接下来我们就能更好地理解为什么函数调用在C语言中是不支持的,而在C++中是可以支持的。
链接时我们需要通过函数名,在合并的符号表中,寻找被调用函数的实际地址。
由于C语言的函数名直接用的就是原来的名字,在编译阶段进行符号汇总时出现两个名字相同的函数就出现了冲突,因此在编译阶段就会报错。
而在C++中,会对相同的函数名称进行名字修饰(name Mangling),从而在调用函数时找到对应的函数地址。
接下来便基于Linux环境下进行演示👇
3.1 C++汇编指令中的函数命名
输入指令使f.cpp
和test.cpp
生成可执行文件tcpp
👇
g++ -o tcpp f.cpp test.cpp
输入指令观察C++汇编指令中的两个函数的名称修饰规则👇
objdump -S tcpp
![](https://img-blog.csdnimg.cn/df70caed313d40cf80da30206d6c046a.png#pic_center)
如上图所示第一个函数的名称为 _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.c
和test.c
生成可执行文件tc
👇
gcc -o tc f.c test.c
输入指令观察C++汇编指令中的两个函数的名称修饰规则👇
objdump -S tc
如上图所示我们发现对应的汇编指令中的函数名称就是以原名称进行命名的。
3.3 总结
- 在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
- 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数名字修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
- 另外我们也理解了,为什么函数重载要求参数不同,而跟返回值没关系。
4.链接静态库/动态库
理解了上面这些知识,我们就可以自行配置静态库进行使用了。对于静态库/动态库具体是做什么的,暂时不需要了解。这里的重点是如何配置一个静态库。
4.1 C++调用C静态库
C++一定是可以调用C++的静态库,那它要如何调用C的静态库呢?
下面用于配置静态库的代码使用的是之前博主写过的“Stack(栈)”的数据结构,对于代码具体写了什么可以不用在意,重要的是配置静态库的过程,以及如何使用C++代码调用C的静态库。
- 创建一个空项目命名为Stack_C,并将代码贴进去看代码点这里→Stack源代码
- 配置静态库
![](https://img-blog.csdnimg.cn/41108c8939d642ca928560f9da047104.png)
![](https://img-blog.csdnimg.cn/eaf321b51dfa4d33972b74c2ccb76683.png)
PS:这里一定要生成一下
![](https://img-blog.csdnimg.cn/4beb842f66144cdba682de51cd7ee6cd.png)
![](https://img-blog.csdnimg.cn/b99e3525f7674ed1bdf5824d8f4cea2b.gif#pic_center)
-
此时就成功生成了一个静态库
Stack_C.lib
了👇
-
随便拷一个需要调用静态库的代码到另外一个项目
test_05_08
项目中👇
-
包含静态库的头文件
首先我们要知道头文件的相对于newtest.cpp
的位置在哪👇
因此,包含静态库我们写下这样的代码👇
#include "../../Stack_C/Stack_C/Stack.h"
如果是VS 2019
还需要附加包含目录,否则可能会出现“无法打开源文件”的问题
我们需要确保外部依赖项中有Stack.h文件👇
下面为附加包含目录的操作👇
上述操作后就成功包含头文件了,此时程序不再出现语法错误。
![](https://img-blog.csdnimg.cn/b99e3525f7674ed1bdf5824d8f4cea2b.gif#pic_center)
- 生成对应的DEBUG
- 附加依赖项
接着就可以点击应用了
![](https://img-blog.csdnimg.cn/b99e3525f7674ed1bdf5824d8f4cea2b.gif#pic_center)
给大家捋一捋上面的操作,首先我们将
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++静态库
- 准备工作
配置静态库的步骤和上面一样,这里就不过多解释了
![](https://img-blog.csdnimg.cn/54918daabd824edd84e836927a399f77.png)
![](https://img-blog.csdnimg.cn/8e691cf536e249028d4f69eb0bf17953.png)
![](https://img-blog.csdnimg.cn/ac79b177f68d468fa9ad817332004e90.png)
![](https://img-blog.csdnimg.cn/0e1846886e6a475a9a5a4a38fc98d8dd.png)
![](https://img-blog.csdnimg.cn/77cb2dc25c5b435dbd0ba7134622dc75.png)
![](https://img-blog.csdnimg.cn/3926ec9e0998488d811a832de67ce92f.png)
问:前面的 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