在 C++ 中,同一作用域下,同一个函数名是可以定义多次的,前提是形参列表不同。这种名字相同但形参列表不同的函数,叫做“重载函数”。这是 C++ 相对 C 语言的重大改进,也是面向对象的基础。
定义重载函数
// 使用指针和长度作为形参
void printArray(const int* arr, int size)
{
for (int i = 0; i < size; i++)
cout << arr[i] << "\t";
cout << endl;
}
// 使用数组引用作为形参
void printArray(const int(&arr)[6])
{
for (int num : arr)
cout << num << "\t";
cout << endl;
}
int main()
{
int arr[6] = {1, 2, 3, 4, 5, 6};
printArray(arr, 6); // 传入两个参数,调用第一种实现
printArray(arr); // 传入一个参数,调用第二种实现
}
这里需要注意:
- 重载的函数,应该在形参的数量或者类型上有所不同
- 形参的名称在类型中可以省略,所以只有形参名不同的函数是一样的
- 调用函数时,编译器会根据传递的实参个数和类型,自动推断使用哪个函数
- 主函数不能重载
有 const 形参时的重载
当形参有 const 修饰时,要区分它对于实参的要求到底是什么,是否要进行值的拷贝。如果是传值参数,传入实参时会发生值的拷贝,那么实参是变量还是常量其实是没有区别的:
void fn(int x);
void fn(const int x); // int常量做形参,与不加const等价
void fun(int* p);
void fun(int* const p); // 指针常量做形参,与不加const等价
这种情况下,const 不会影响传入函数的实参类型,所以跟不加 const 的定义是一样的;这叫做“顶层 const”。这时两个函数相同,无法进行函数重载。
另一种情况则不同,那就是传引用参数。这时如果有 const 修饰,就成了“常量的引用”;对于一个常量,只能用常量引用来绑定,而不能使用普通引用。类似地,对于一个常量的地址,只能由“指向常量的指针”来指向它,而不能用普通指针。
void fn(int &x);
void fn(const int & x); // 形参类型是常量引用,这是一个新函数
void fun(int* p);
void fun(const int* p); // 形参类型是指向常量的指针,这是一个新函数
这种情况下,const 限制了间接访问的数据对象是常量,这叫做“底层 const”。当实参是常量时,不能对不带 const 的引用进行初始化,所以只能调用常量引用做形参的函数;而如果实参是变量,就会优先匹配不带 const 的普通引用:这就实现了函数重载。
函数匹配
如果传入的实参跟形参类型不同,只要能通过隐式类型转换变成需要类型,函数也可以正确调用。那假如有几个不同的重载函数,它们的形参类型可以进行自动转换,这时传入实参应该调用哪个函数呢?例如:
void f();
void f(int x);
void f(int x, int y);
void f(double x, double y = 1.5);
f(3.14); // 应该调用哪个函数?
确定到底调用哪个函数的过程,叫做“函数匹配”。
(1)候选函数
函数匹配的第一步,即确定“候选函数”,也就是先找到对应的重载函数集。候选函数有两个要求:
- 与调用的函数同名
- 函数的声明,在函数的调用点是可见的
所以上述例子中,一共有 4 个叫做 f 的函数,它们都为候选函数。
(2)可行函数
接下来需要从候选函数中,选出跟传入的实参匹配的函数。这些函数叫做“可行函数”。可行函数也有两个要求:
- 形参个数与调用传入的实参数量相等
- 每个实参的类型与对应形参的类型相同,或者可以转换成形参的类型
上述例子中,传入的实参只有一个,是一个 double 类型的字面值常量,所以可以排除 f()
和 f(int, int)
。而剩下的 f(int)
和 f(double, double = 1.5)
都是匹配的,所以有 2 个可行函数。
(3)寻找最佳匹配
最后就是在可行函数中,选择最佳匹配。简单来说,实参类型与形参类型越接近,它们就匹配得越好。所以,能不进行转换就实际匹配的,要优于需要转换的。
上面的例子中,f(int)
必须要将 double 类型的实参转换成 int,而 f(double, double = 1.5)
不需要,所以后者是最佳匹配,最终调用的就是它。第二个参数会由默认实参 1.5 来填补。
(4)多参数的函数匹配
如果实参的数量不止一个,那么就需要逐个比较每个参数;同样,类型能够精确匹配的要优于需要转换的。这时寻找最佳匹配的原则如下:
- 如果可行函数的所有形参都能精确匹配实参,那么它就是最佳匹配
- 如果没有全部精确匹配,那么当一个可行函数所有参数的匹配,都不比别的可行函数差、并且至少有一个参数要更优,那它就是最佳匹配
(5)二义性调用
如果检查所有实参之后,有多个可行函数不分优劣、无法找到一个最佳匹配,那么编译器会报错,这被称为“二义性调用”。例如:
f(10, 3.14); // 二义性调用
这时的可行函数为 f(int, int)
和 f(double, double = 1.5)
。第一个实参为 int 类型,f(int, int)
占优;而第二个实参为 double 类型,f(double, double = 1.5)
占优。这时两个可行函数分不出胜负,于是就会报二义性调用错误。
重载与作用域
重载是否生效,跟作用域是有关系的。如果在内层、外层作用域分别声明了同名的函数,那么内层作用域中的函数会覆盖外层的同名实体,让它隐藏起来。
不同的作用域中,是无法重载函数名的。
#include<iostream>
using namespace std;
void print(double d)
{
cout << "d: " << d << endl;
}
void print(string s)
{
cout << "s: " << s << endl;
}
int main()
{
// 调用之前做函数声明
void print(int i);
print(10);
print(3.14); // 将3.14转换为3,然后调用
//print("hello"); // 错误,找不到对应参数的函数定义
system("pause");
}
void print(int i)
{
cout << "i: " << i << endl;
}
如果想让函数正确地重载,应该把函数声明放到同一作用域下:
#include<iostream>
using namespace std;
void print(int i)
{
cout << "i: " << i << endl;
}
void print(double d)
{
cout << "d: " << d << endl;
}
void print(string s)
{
cout << "s: " << s << endl;
}
int main()
{
print(10);
print(3.14); // 将3.14转换为3,然后调用
print("hello");
system("pause");
}
C++的函数重载是一项强大而实用的特性,它允许在同一作用域中定义多个同名函数,通过参数的类型和数量的不同进行区分。这为代码编写提供了更灵活和可读性更好的选择。点赞!
很有价值的总结,加深对重载的理解👍