"Key Takeaway"
- 函数中用于区分重载函数的部分有:形参个数、形参类型、省略号。
- 返回值类型不能被用来区分函数
- 成员函数在上面的基础上还包括
const
、volatile
和引用限定符- 当编译器编译一个函数时,它会执行命名修饰(Name mangling),这意味着根据各种条件改变编译后的函数名称(“改写”),例如参数的数量和类型,以便链接器有唯一的名称来处理。
在上一节课中 (8.9 - 函数重载), 我们引入了函数重载的概念,使用函数重载可以创建具有相同名称的多个函数,前提是每个相同名称的函数具有不同的形参类型(或者可以用其他方式区分函数)。
在这一课中,我们将进一步了解重载函数是如何区分开来的,不能被正确区分的重载函数将导致编译器发出编译错误。
重载函数是如何被区分的
函数属性 | 被用于区分函数 | 备注 |
---|---|---|
形参个数 | Yes | |
形参类型 | Yes | 不包括 typedefs 、类型别名、const 限定符。包括省略号(ellipses) |
返回值类型 | No |
注意,函数的返回类型不用于区分重载函数。我们稍后再讨论这个问题。
"扩展阅读"
对于成员函数,还会考虑附加的函数级限定符:
函数级限定符 是否用于重载 const
orvolatile
Yes 引用限定符(ref-qualifiers) Yes 例如,
const
成员函数可以与其他完全相同的非const
成员函数区别开来(即使它们共享相同的形参集)。
基于参数个数进行重载
只要每个重载函数具有不同数量的形参,就可以区分重载函数。例如:
int add(int x, int y)
{
return x + y;
}
int add(int x, int y, int z)
{
return x + y + z;
}
编译器可以很容易地将两个整型参数的函数调用匹配到 add(int, int)
,而将三个整型参数的函数调用匹配到 add(int, int, int)
。
基于参数类型进行重载
只要每个重载函数的形参类型列表是不同的,就可以对函数进行区分。例如,以下所有的重载函数都是可以被区分的:
int add(int x, int y); // 整型版本
double add(double x, double y); // 浮点版本
double add(int x, double y); // 混合版本
double add(double x, int y); // 混合版本
因为类型别名(或 typedefs
)并不会产生不同的类型,所以使用类型别名的重载函数与使用对应类型的重载没有区别。例如,以下所有重载都是不能被区分的(并将导致编译错误):
typedef int height_t; // typedef
using age_t = int; // type alias
void print(int value);
void print(age_t value); // not differentiated from print(int)
void print(height_t value); // not differentiated from print(int)
对于通过值传递的参数,也不考虑 const
限定符。因此,以下函数是不能被区分开的:
void print(int);
void print(const int); // not differentiated from print(int)
"扩展阅读"
我们还没有涉及到省略号,但是省略号参数被认为是一种独特的参数类型:
void foo(int x, int y); void foo(int x, ...); // 和 foo(int, int)是不同的
返回值类型不能被用来区分函数
在区分重载函数时,不考虑函数的返回类型。
考虑这样一种情况:您想编写一个返回随机数的函数,但是您需要一个返回整数的版本,以及另一个返回双精度数的版本。你可能会忍不住这样做:
int getRandomValue();
double getRandomValue();
Visual Studio 2019 中会产生如下编译错误:
error C2556: 'double getRandomValue(void)': overloaded function differs only by return type from 'int getRandomValue(void)'
这是很显然的,假设你自己是编译器,当你看到下面的定义时:
getRandomValue();
你应该调用两个函数中的哪个呢?
"题外话"
这是一个有意的选择,因为它确保了函数调用的行为可以独立于表达式的其他部分来确定,从而使理解复杂表达式更加简单。换句话说,我们总是可以仅根据函数调用中的参数来决定调用哪个版本的函数。如果返回值用于区分,那么我们就没有一个简单的语法方法来判断调用了哪个函数的重载——我们还必须理解返回值是如何被使用的,这需要更多的分析。
解决这个问题的最好方法是给函数取不同的名字:
int getRandomInt();
double getRandomDouble();
类型签名
函数的 类型签名(type signature) (通常简称为签名) 是函数头的一部分,被用于区分函数。在 C++ 中,签名包括函数名、参数数量、参数类型和函数级的限定符。值得注意的是,它不包括返回类型。
命名修饰
"题外话"
当编译器编译一个函数时,它会执行命名修饰(Name mangling),这意味着根据各种条件改变编译后的函数名称(“改写”),例如参数的数量和类型,以便链接器有唯一的名称来处理。
例如,一些原型为
int fcn()
的函数可能会被编译为__fcn_v
,而int fcn(int)
可能会被编译为__fcn_i
。因此,虽然在源代码中,两个重载函数共享一个名称,但在编译代码中,它们的名称实际上是唯一的。标准没有规定名称应该如何被修饰,因此不同的编译器将产生不同的函数名称。