【C++】函数重载的形式及其背后原理
常言道:中国有俩球,谁都赢不了!这句话在不同的语境下有不同的意思
C++中,函数支持在同一作用域下声明几个功能类似的同名函数,但需要遵守以下规定……
- 形参个数不同
- 形参类型不同
- 形参类型的顺序不同
- 注意:只修改函数返回值不构成重载
编译器会在调用这些同名函数的时候,根据具体情况来选择不同的函数
[TOC]
1.函数重载的样式
上面提到了函数重载的3个规定,下面让我们来用具体示例认识一下它们
假设我们需要一个A+B的代码,如果每次都需要根据不同数据类型来写不同的函数去实现这个功能,未免有点太过繁杂。
在C++中,只需要修改函数的参数,即构成了函数重载,编译器就会自己选择对应的函数进行相加操作
1.1形参类型不同
1 | //函数重载 |
1.2形参个数不同
1 | int Add(int a, int b) { |
1.3形参类型顺序不同
这里的顺序并不是a和b的顺序哈!只把a和b换一个位置是不构成函数重载的
这里指的是先传int再传double,和先传double再传int的两种函数
1 | //形参类型的顺序不同 |
1.4返回值不同非重载
只修改函数的返回值类型是不构成函数重载的
2.C++实现函数重载的原理
在看后续内容之前,建议先复习一下程序运行的4个阶段,以便理解后面的操作👉传送门
这里使用我的树莓派在Linux系统下给大家演示一下函数重载背后的样式
首先我创建了3个文件,test.c Add.h Add.cpp
,文件的内容一并给出
1 | //Add.h |
可以看到,这里我们使用gcc
这个C语言编译器编译程序的时候,出现了很多报错,因为C语言是不支持函数重载的
2.1编译生成可执行文件
需要使用g++
编译器来编译这个代码
1 | g++ test.cpp add.cpp -o Tcpp |
执行./Tcpp
运行该函数,可以看到正常输出了相加后的结果
2.2查看汇编
接下来我们要使用另外一个命令来查看可执行文件Tcpp的汇编代码
1 | objdump -S Tcpp |
在这里面可以找到我们两个Add函数的位置,可见它们的地址是不同的,并且一个函数名为_Z3Addii
,另外一个是_Z3Adddd
2.2.1汇编函数名的含义
这两个汇编代码中的函数名,其实包含了函数名、函数参数这两个信息
拆分了其中一个,那么另外一个_Z3Adddd
的意思就很明确了,末尾的两个d代表函数参数是(double,double)
我们可以创建另外一个文件,查看它的汇编代码,进一步确认命名规则(其实这个命名规则是反推得出的)
1 |
|
编译程序后,执行objdump -S
,可以看到f函数被命名为_Z1fii
,代表函数名长度为1,原本函数名f,和函数参数(int ,int)
现在我们知道了汇编中这个函数名的命名规则,那它和C++支持函数重载有什么关系呢?
在这之前,我们还需看看c语言程序,汇编代码中函数又是怎么命名的
2.2.2查看C语言汇编
这里我把之前的函数修改成了C语言的样式,gcc
编译后再来看看它的汇编
1 | void f(int a,int b){ |
然后你就会发现,C语言汇编代码中的函数名,就是函数原本的名字f
,没有添加任何东西!
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++在汇编处理中能够以这种命名方式来区分同名的不同函数,并给它们赋予不同的地址,编译器在链接符号表的时候,才能通过函数传参的不同找到它需要调用的对应函数的地址
在main函数的汇编中,也能找到对应函数的调用操作
3.语法extern”C”
因为C++汇编处理中对函数名的修饰和C语言不同,所以C++中有这么一个语法,专门用来告诉编译器,某某某函数要用C语言的规则来修饰
1 |
|
可以看到,使用这种方式修饰的fun
函数,在汇编中就只有函数名,而不是C++形式原本的_Z3funii
这样C语言的代码就可以链接这种方式写的C++静态库(前提是这个静态库中没有函数重载和C++的语法)
然后我就想问:这和C的静态库有啥区别……
当然有了!一个库里面有很多很多代码,总有些函数接口是C语言也能支持的嘛,这些接口就用C语言的方式来修饰,这样C语言也能调用了,不一举两得?
3.1C++调用C语言静态库
除了更改修饰方式外,extern"C"
还用于让C++程序来调用C语言写的库
比如树莓派要用到的
wiringPi库
,它是用C语言实现的,在编程为静态库后,里面汇编对函数的修饰就固定了,并没有C++下的_Z1...
和参数类型修饰。
这时候如果用C++直接来调用这个函数,C++程序是找不到对应的函数的。在这种情况下,extern"C"
的作用就是让编译器以C语言的方式去寻找对应函数
比如下图的代码,调用了wiringPi
库里面的初始化函数,是最常用的一个函数
我们用G++编译器编译这个代码,就会发现,欸tnnd怎么没有报错啊?
其实吧,库函数的开发者早就想到了这一点。在平日编程中,也有办法来解决这个问题——那就是用条件编译指令!
3.2用条件编译解决问题
前情提要:在C++的编译环境中有一个预定义符号__cplusplus
1 | cout<< __cplusplus <<endl; |
在linux环境下,编译器打印出了以下数字
而在windows的VS2019编译器下打印了下面的数字
咱先不管这个数字是啥意思(看起来是一个日期),至少在C语言中是没有这个预定义符号的
这样我们就可以利用这个预定义符号,假设是C++环境,就放出extern"C"
来声明函数,如果是C语言环境,就不用extern"C"
方法一:批量extern
1 |
|
方法二:define一个符号为extern"C"
,然后在每一个定义前面单独加
1 |
|
这样不管是C语言,还是C++的程序,都能正常建立符号表,找到对应的函数
Github:wiringPi库源码仓库
可以看到,大佬当初编写wiringPi库的时候就用了这个方法,这也是为什么在我的树莓派上,G++编译器也能直接识别出wiringPi库的原因
在找这部分资料的时候,还发现了一个小故事:
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语言也支持它
但是我还发现,有些带有C++的函数接口,也用了extern "C"
,那是不是我们上面的结论错了呢?
实践出真知!
4自己整一个静态库
4.1C++调用C语言静态库
首先创建一个VS的空项目,把我之前写的C语言单链表代码放进去
右键这里的项目名称-属性,然后在配置属性-常规-配置类型
中,把项目改成静态库
修改完毕后,编译程序,你会发现debug目录下多了静态库文件.lib
然后在我们当前的C++项目中,修改项目属性-链接器-常规-附加库目录
和项目属性-链接器-输入-附加依赖项
最后以#include "../Slist/Slinklist.h"
的形式引用静态库
你会发现直接引用是会报错的,因为这个单链表的库是用C语言写的,我们没有使用extern "C"
来引用
使用了之后,程序正常调用了C语言的库,并打印出了结果!
4.2C语言调用C++静态库
接着,我们再写一个简单的C++程序,用上面同样的方法编译成静态库,并在C语言的项目中调用它
可以看到,这个没有任何C++语法的C++静态库被正常调用并打印出了结果
如果我们不使用extern "C"
,C语言项目就无法正常使用该静态库
而当我们在C++的静态库中包含C++的头文件后,C语言项目中也报错了!
1 |
|
光是链接库函数头文件和命名空间就报错了,那不能使用带C++语法的函数也是板上钉钉的事情了!
而在具有extern "C"
属性的路径中,也不能包含函数重载,VS会报错
在头文件中定义自己的命名空间,在C语言项目中也是无法通过编译的
1 | namespace muxue { |
现在可以确认我们的结论,只有不包含任何C++的语法和函数重载的C++静态库,才能正常被C语言项目调用!
勘误,上述结论错误
22-05-06,在同学的提示下,发现了这个错误
之前C调用C++的方式有问题,因为我是直接把C++的语法放到了头文件中,在展开的时候C程序编译会报错
但如果把C++的语法放入cpp文件,头文件中不包含的话,就不会报错了!
可以看到在最后的测试项目中,C语言程序成功调用了c++的语法并正确输出了内容
这也能解释我关于谷歌TCmalloc库的疑惑了,看来C和C++真的是互通有无啊!
结语
本篇笔记详细解释了C++中函数重载的类型,以及背后的实现原理。
这个博客花了我整整4小时的时间,感觉很充实!
所以求个赞不过分吧!谢谢大家!