常言道:中国有俩球,谁都赢不了!这句话在不同的语境下有不同的意思

C++中,函数支持在同一作用域下声明几个功能类似的同名函数,但需要遵守以下规定……

  • 形参个数不同
  • 形参类型不同
  • 形参类型的顺序不同
  • 注意:只修改函数返回值不构成重载

编译器会在调用这些同名函数的时候,根据具体情况来选择不同的函数


[TOC]

1.函数重载的样式

上面提到了函数重载的3个规定,下面让我们来用具体示例认识一下它们

假设我们需要一个A+B的代码,如果每次都需要根据不同数据类型来写不同的函数去实现这个功能,未免有点太过繁杂。

在C++中,只需要修改函数的参数,即构成了函数重载,编译器就会自己选择对应的函数进行相加操作

1.1形参类型不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//函数重载
int Add(int a, int rb){
return a + b;
}
long Add(long a, long b){
return a + b;
}
double Add(double a, double b){
return a + b;
}

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

image-20220429185145046

1.2形参个数不同

1
2
3
4
5
6
7
int Add(int a, int b) {
return a + b;
}
//个数不同
int Add(int a, int b, int c) {
return a + b + c;
}

image-20220429185334477

1.3形参类型顺序不同

这里的顺序并不是a和b的顺序哈!只把a和b换一个位置是不构成函数重载的

这里指的是先传int再传double,和先传double再传int的两种函数

1
2
3
4
5
6
7
8
9
//形参类型的顺序不同
void Add(int a, double b) {
cout << a << endl;
cout << b << endl;
}
void Add(double a,int b) {
cout << a << endl;
cout << b << endl;
}

image-20220429185748785


1.4返回值不同非重载

只修改函数的返回值类型是不构成函数重载的

image-20220429185949622


2.C++实现函数重载的原理

在看后续内容之前,建议先复习一下程序运行的4个阶段,以便理解后面的操作👉传送门

这里使用我的树莓派在Linux系统下给大家演示一下函数重载背后的样式

首先我创建了3个文件,test.c Add.h Add.cpp,文件的内容一并给出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Add.h
#include<iostream>
using namespace std;
int Add(int a,int b);
double Add(double a,double b);

//Add.c
#include "Add.h"

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

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

//test.cpp
#include "Add.h"

int main()
{
cout<<Add(1,2)<<endl;
cout<<Add(1.5,2.5)<<endl;

return 0;
}

可以看到,这里我们使用gcc这个C语言编译器编译程序的时候,出现了很多报错,因为C语言是不支持函数重载的

image-20220429194238128

2.1编译生成可执行文件

需要使用g++编译器来编译这个代码

1
g++ test.cpp add.cpp -o Tcpp

执行./Tcpp运行该函数,可以看到正常输出了相加后的结果

image-20220429194548261


2.2查看汇编

接下来我们要使用另外一个命令来查看可执行文件Tcpp的汇编代码

1
objdump -S Tcpp

在这里面可以找到我们两个Add函数的位置,可见它们的地址是不同的,并且一个函数名为_Z3Addii,另外一个是_Z3Adddd

image-20220429194758864

2.2.1汇编函数名的含义

这两个汇编代码中的函数名,其实包含了函数名、函数参数这两个信息

image-20220429195153935

拆分了其中一个,那么另外一个_Z3Adddd的意思就很明确了,末尾的两个d代表函数参数是(double,double)


我们可以创建另外一个文件,查看它的汇编代码,进一步确认命名规则(其实这个命名规则是反推得出的)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
using namespace std;

void f(int a,int b){
cout <<a<<endl;
cout <<b<<endl;
}

int main()
{
f(3,4);
return 0;
}

编译程序后,执行objdump -S,可以看到f函数被命名为_Z1fii,代表函数名长度为1,原本函数名f,和函数参数(int ,int)

image-20220429195810955

现在我们知道了汇编中这个函数名的命名规则,那它和C++支持函数重载有什么关系呢?


在这之前,我们还需看看c语言程序,汇编代码中函数又是怎么命名的

2.2.2查看C语言汇编

这里我把之前的函数修改成了C语言的样式,gcc编译后再来看看它的汇编

1
2
3
void f(int a,int b){
printf("%d %d\n",a,b);
}

image-20220429200244080

然后你就会发现,C语言汇编代码中的函数名,就是函数原本的名字f,没有添加任何东西!

image-20220429200356998

2.3得出结论

看到这里,你能猜出来为什么C++支持函数汇编,而C语言不支持了吗?

没错!那是因为C++的汇编代码中,函数名还保存了函数的形参类型,而C语言中并没有保存,自然无法区分两个函数

这个汇编函数名的命名方式也能解释C++函数重载的3种样式

假设我们有一个fun函数,那么我们可以推断出它的汇编函数名

类型形式一形式二
形参个数不同_Z3funii(int,int)_Z3funiii(int,int,int)
形参类型不同_Z3funii(int,int)_Z3fundd(double,double)
形参类型顺序不同_Z3funid(int,double)_Z3fundi(double,int)

同时也能解释为何只修改函数返回值类型是不构成重载的,因为汇编代码中没有保存函数的返回值


正因为C++在汇编处理中能够以这种命名方式来区分同名的不同函数,并给它们赋予不同的地址编译器在链接符号表的时候,才能通过函数传参的不同找到它需要调用的对应函数的地址

image-20220429202120839

在main函数的汇编中,也能找到对应函数的调用操作

image-20220429202215847


3.语法extern”C”

因为C++汇编处理中对函数名的修饰和C语言不同,所以C++中有这么一个语法,专门用来告诉编译器,某某某函数要用C语言的规则来修饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

extern "C" int fun(int a,int b);

int main()
{
int sum=fun(1,2);
return 0;
}

int fun(int a,int b){
return a+b;
}

可以看到,使用这种方式修饰的fun函数,在汇编中就只有函数名,而不是C++形式原本的_Z3funii

image-20220429210808692

这样C语言的代码就可以链接这种方式写的C++静态库(前提是这个静态库中没有函数重载和C++的语法)

然后我就想问:这和C的静态库有啥区别……

当然有了!一个库里面有很多很多代码,总有些函数接口是C语言也能支持的嘛,这些接口就用C语言的方式来修饰,这样C语言也能调用了,不一举两得?

3.1C++调用C语言静态库

除了更改修饰方式外,extern"C"还用于让C++程序来调用C语言写的库

比如树莓派要用到的wiringPi库,它是用C语言实现的,在编程为静态库后,里面汇编对函数的修饰就固定了,并没有C++下的_Z1...和参数类型修饰。

这时候如果用C++直接来调用这个函数,C++程序是找不到对应的函数的。在这种情况下,extern"C"的作用就是让编译器以C语言的方式去寻找对应函数

比如下图的代码,调用了wiringPi库里面的初始化函数,是最常用的一个函数

image-20220429215242153

我们用G++编译器编译这个代码,就会发现,欸tnnd怎么没有报错啊?

image-20220429215353327

QQ图片20220419102802

其实吧,库函数的开发者早就想到了这一点。在平日编程中,也有办法来解决这个问题——那就是用条件编译指令!

3.2用条件编译解决问题

前情提要:在C++的编译环境中有一个预定义符号__cplusplus

1
cout<< __cplusplus <<endl;

在linux环境下,编译器打印出了以下数字

image-20220429220017411

而在windows的VS2019编译器下打印了下面的数字

image-20220429220319680

咱先不管这个数字是啥意思(看起来是一个日期),至少在C语言中是没有这个预定义符号的

image-20220429220529989

这样我们就可以利用这个预定义符号,假设是C++环境,就放出extern"C"来声明函数,如果是C语言环境,就不用extern"C"

方法一:批量extern

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef __cplusplus
extern "C" {
#endif

void Add(int a,int b);
int fun(int a,int b);
//这里可以放多个函数的声明
//……

#ifdef __cplusplus
}
#endif

方法二:define一个符号为extern"C",然后在每一个定义前面单独加

1
2
3
4
5
6
7
8
9
10
#ifdef __cplusplus
#define EXTERN extern "C"
#else
#define EXTERN
#endif

EXTERN void Add(int a,int b);
EXTERN int fun(int a,int b);
EXTERN int func(int a,int b);
//……

这样不管是C语言,还是C++的程序,都能正常建立符号表,找到对应的函数

Github:wiringPi库源码仓库

可以看到,大佬当初编写wiringPi库的时候就用了这个方法,这也是为什么在我的树莓派上,G++编译器也能直接识别出wiringPi库的原因

image-20220429221236754

在找这部分资料的时候,还发现了一个小故事:wiringPi库现在已经不官方开源了。因为有很多初学者拿代码去烦原作者(于是作者在官网上写了“这不是给初学者玩的”告示)还有很多人倒卖他写的库,所以他就在最后一次公开后,停止了官方开源

3.3C语言调用C++的库

同理,有的时候我们也会用C语言来调用C++的库

但是!就如我上头说的,这个库里面,可以供C语言调用的函数不能有C++的语法和函数重载

Github:TcMalloc代码仓库

比较好的一个例子是谷歌的tcmalloc库:此存储库包含TCMalloc的C++代码。 TCMalloc是谷歌对C的malloc()和C++运算符的定制实现,用于在我们的C和C++代码中分配内存。TCMalloc是一个快速的多线程malloc实现。


整个库的函数入口是在tcmalloc.cc中定义的,打开它可以看到,虽然大部分代码都是用C++实现的,但是少部分函数接口因为没有C++的语法,所以使用了extern "C"让C语言也支持它

image-20220429222658895

但是我还发现,有些带有C++的函数接口,也用了extern "C",那是不是我们上面的结论错了呢?

image-20220429223300422

实践出真知

4自己整一个静态库

4.1C++调用C语言静态库

首先创建一个VS的空项目,把我之前写的C语言单链表代码放进去

image-20220429223816858

右键这里的项目名称-属性,然后在配置属性-常规-配置类型中,把项目改成静态库

image-20220429224022321

修改完毕后,编译程序,你会发现debug目录下多了静态库文件.lib

image-20220429225627216

然后在我们当前的C++项目中,修改项目属性-链接器-常规-附加库目录项目属性-链接器-输入-附加依赖项

image-20220429225404293

image-20220429225441335

最后以#include "../Slist/Slinklist.h"的形式引用静态库

你会发现直接引用是会报错的,因为这个单链表的库是用C语言写的,我们没有使用extern "C"来引用

image-20220429224245378

使用了之后,程序正常调用了C语言的库,并打印出了结果!

image-20220429225307159

4.2C语言调用C++静态库

接着,我们再写一个简单的C++程序,用上面同样的方法编译成静态库,并在C语言的项目中调用它

image-20220429230736776

可以看到,这个没有任何C++语法的C++静态库被正常调用并打印出了结果

image-20220429230658140

如果我们不使用extern "C",C语言项目就无法正常使用该静态库

image-20220429230922301

而当我们在C++的静态库中包含C++的头文件后,C语言项目中也报错了!

1
2
#include <iostream>
using namespace std;

光是链接库函数头文件和命名空间就报错了,那不能使用带C++语法的函数也是板上钉钉的事情了!

image-20220429231028543

而在具有extern "C"属性的路径中,也不能包含函数重载,VS会报错

image-20220429231321433

在头文件中定义自己的命名空间,在C语言项目中也是无法通过编译的

1
2
3
4
namespace muxue {
int a = 0;
}
using namespace muxue;

image-20220429231603592

现在可以确认我们的结论,只有不包含任何C++的语法和函数重载的C++静态库,才能正常被C语言项目调用!

勘误,上述结论错误

22-05-06,在同学的提示下,发现了这个错误

之前C调用C++的方式有问题,因为我是直接把C++的语法放到了头文件中,在展开的时候C程序编译会报错

但如果把C++的语法放入cpp文件,头文件中不包含的话,就不会报错了!

image-20220506152800090

可以看到在最后的测试项目中,C语言程序成功调用了c++的语法并正确输出了内容

image-20220506152923834

这也能解释我关于谷歌TCmalloc库的疑惑了,看来C和C++真的是互通有无啊!


结语

本篇笔记详细解释了C++中函数重载的类型,以及背后的实现原理。

这个博客花了我整整4小时的时间,感觉很充实!

所以求个赞不过分吧!谢谢大家!

QQ图片20220424132540