函数重载在很多语言中都存在,那么什么是函数重载呢?为什么要有函数重载?
带着这个问题,我们来进行一次思考…
首先从很多书本上都可以看到函数重载的概念:函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。
重载函数通常用来命名一组功能相似的函数,这样做减少了命名污染,对程序可读性有很大的提示,也间接降低了维护的成本。
我们来一段代码体会一下函数重载:
#include<iostream>
#include<string>
using namespace std;
void Print(int i)
{
cout<<i<<endl;
}
void Print(string str)
{
cout<<str<<endl;
}
void Print(char c)
{
cout<<c<<endl;
}
int main()
{
Print(10);
Print("hello overload");
Print('C');
return 0;
}
通过上面的代码,编译器会自动的为我们匹配到最适合的函数。
为什么需要函数重载?
- 在没有函数重载的情况下,我们实现上面函数的功能只能一个一个的去起名字,比如:Print_i,Print_St,Print_c。打过网游的朋友们都知道,起名字很难搞的!!!所以,这就是函数重载的作用之一!
- 类的构造函数和类名相同,所以所有的构造函数名字都相同。如果没有重载,要实例化不同的对象,那你又得忙了~
- 操作符重载,实际上就是函数重载,它可以使我们自行定义已有操作符的含义。
既然已经晓得了函数重载有这么多好处,那么我们来说说它是如何实现的?
我们来研究一下它底层的汇编代码,在调用的时候,是如何解析这些函数的!(下面的代码都在Linux系统Ubuntu16.0分支进行实验)
把上面的代码用g++ -S 进行汇编,然后观察函数名编译之后为:
- void Print(int i)->_Z5Printi
- void Print(String str)->_Z5PrintNSt7
- void Print(char c)->_Z5Printc
我们可以发现,重载函数的名字全都变了,名字不在是我们当初写的了,那么名字是通过怎样的条件变换的?看完上面的名字,我们猜想可能是原本的函数名+参数列表,因为在定义中没有返回值这一项。但是进一步猜想,前面的_Z5是什么意思,为什么要放在前面呢?也许要加上返回值才对,那就是返回值类型+函数名+参数列表。
我们可以通过代码测试一下自己的猜想,把返回值换成别的。
int Add(int a, int b)
{
return a+b;
}
double Add(double a, double b)
{
return a+b;
}
汇编结果如下:
- int Add(int a, int b)-> _Z3Addii
double Add(double a, double b)->_Z3Adddd
果然如此,并且大胆的猜测,函数参数列表中的int可能对应i,double可能对应d,char可能对应c,诸如此类的…而前面的Z+数字对应的可能是返回类型。
既然返回类型也在我们的命名改变的映射机制之中,那么为什么不将函数返回值类型考虑到函数重载之中呢? ——这是为了保持解析操作符或函数调用时,独立于上下文。
如果返回值类型考虑到函数重载中,这样将不可能再独立于上下文决定调用哪个函数,也就是说,当返回值类型与接收参数的类型不一致时,就不是编译器判断的范围了。我们的编程语言和人类语言很大的区别就是,编程语言采用上下文无关文法,而人类语言通常通过上下文推断其含义。
至此我们已经明白了函数重载的过程。等等,我们是不是少了点什么?作用域呢?
作用域当然是不可少的一环,我们依然通过代码来解析:
#include<iostream>
#include<string>
using namespace std;
class test{
public:
void Print(int i)
{
cout<<i<<endl;
}
void Print(string str)
{
cout<<str<<endl;
}
void Print(char c)
{
cout<<c<<endl;
}
};
int main()
{
test t;
t.Print(10);
t.Print("hello overload");
t.Print('C');
return 0;
}
来看看它映射后的函数名:
- void Print(int i)->_ZN4test5PrintEi
- void Print(string str)->_ZN4test5PrintENST
- void Print(char c)->_ZN4test5PrintEc
我们通过和前面的对比就可以知道,N4应该表示作用域。那么我们的结论又要改一改了,编译器对命名的映射机制为:作用域+返回值类型+函数名+参数列表
现在已经解决了一大问题,在定义完函数重载之后,用函数名调用的时候是如何去解析和匹配最佳重载函数的:
- 精确匹配:参数完全匹配,包括顺序、类型、const、volatile
- 提升匹配:即整数类型的提升(bool到int、char到int等),float到double
- 使用标准转换匹配:如隐式类型转换
- 使用用户自定义匹配:操作符重载
- 使用省略号匹配:可变参数列表
如果通过重载的机制匹配到多个函数,调用就被拒绝,向上抛出异常。