函数重载

重载(Overloading)是 C++ 为允许同名函数使用多种参数列表以及多种实现版本而提供的机制,C语言是不支持的。 具体来说,函数重载指在同一个作用域中,定义多个具有相同名称但参数列表不同(个数或类型不同)的函数,这一组函数会构成相互重载的关系, 它们之间可以具有相同或不同的返回值类型。

例如

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

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

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

int add(int a, int b, int c) { return a + b + c; }

int main() {
std::cout << add(1, 2) << "\n";
std::cout << add(1.5, 2.3) << "\n";
std::cout << add(1, 2, 3) << "\n";
return 0;
}

在实际使用重载函数时,C++ 编译器会根据传入参数的个数和类型等,查找匹配对应的实现版本,这个过程完全发生在编译期,可能有如下结果:

  • 如果只存在一个版本的函数使得调用过程合法,那么就会匹配这个函数;
  • 如果存在多个版本的函数都可以合法匹配到,那么编译器会进行重载决议,以挑选最合适的版本:(返回值类型完全不影响决议)
    1. 如果实参和形参的类型精确对应,那么就是最理想的情况,最优先选择这个版本;
    2. 如果实参的类型可以自动转换成形参的类型,那么优先选择这个版本;
    3. 如果实参的类型可以按照用户自定义转换方式转换成形参的类型,那么可以选择这个版本;
  • 如果不存在合适的版本,那么编译器在编译时会报错。

如果涉及到模板函数,对应的重载决议会更加复杂,还涉及到模板类型的匹配问题。

在面向对象的部分,同一个类中的不同方法也只是特殊的函数,因此也可以构成重载的关系。 尤其是 C++ 要求构造函数必须与类同名,因此提供多个构造函数时,必然构成重载的关系,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
private:
std::string name;
int age;

public:
Person() : name("Unknown"), age(0) {}

explicit Person(std::string name)
: name(std::move(name)), age(0) {}

Person(std::string name, int age)
: name(std::move(name)), age(age) {}
};

函数标识符

C++为了实现函数重载,一个必要的准备就是让编译器可以区分那些同名但是参数列表不同的函数,这就自然产生了函数标识符的概念。

在C语言中,可以直接通过函数名称来区分不同函数,但是对于C++来说,必须要给函数标识符加上额外的前缀和后缀,以区分相互重载的不同函数。 例如下面这几个函数

1
2
3
void func(int, int);
void func(int, double);
void func(double, double);

g++为这些函数实际生成的函数标识符如下:

1
2
3
func(int, int) -> _Z4funcii
func(int, double) -> _Z4funcid
func(double, double) -> _Z4funcdd

而MSVC为这些函数实际生成的函数标识符如下

1
2
3
func(int, int) -> ?func@@YAXHH@Z
func(int, double) -> ?func@@YAXHD@Z
func(double, double) -> ?func@@YAXDD@Z

由此可见,C++的标识符生成规则大致为: 在函数名称的基础上,通过特定的前缀和后缀加入额外信息,分别对应参数数量、参数类型、参数顺序以及函数名称的长度等信息。

注意:

  • 函数标识符的信息是不包括参数默认值的,编译器不会将参数默认值视为函数签名的一部分。
  • 正如上面的例子,GCC/LLVM和MSVC编译器虽然都考虑到了要把参数信息用于标识符的生成,但是编译器具体使用的生成规则是不一样的,这导致了MSVC得到的库通常无法和另外两个混用!对于C++的库,考虑到兼容性,最稳妥的办法就是使用extern "C"包裹以导出C语言版本的函数接口,这会让C++编译器采用C语言的规则来生成函数标识符。(此时自然就不再支持函数重载,以及其它的部分C++语法了)