C++ 函数重载

在实际开发中,我们经常会遇到几个函数做的其实是同一件事,只是接收的参数个数或类型不同。比如打印数据时,有时需要打印 int 整数,有时需要打印 double 小数。

如果每种情况都单独起一个名字,比如写成 printInt()、printDouble(),调用时你就得去记一堆函数名。为了解决这个问题,C++ 提供了 “函数重载(function overloading)”,让功能相似、只是参数不同的多个函数共用同一个名字。

什么是函数重载?

在 C++ 中,函数重载指的是在同一个作用域内定义多个名字相同、但参数列表不同的函数。当我们调用该函数时,编译器会根据实际传入的实参,自动匹配并执行对应的那个版本。

要让多个同名函数构成合法的重载,它们的参数列表必须至少满足以下 3 个条件中的一个:

  • 参数个数不同:比如一个函数接收 2 个参数,另一个同名函数接收 3 个参数。
  • 参数类型不同:比如一个函数接收 int 类型参数,另一个同名函数接收 double 类型参数。
  • 不同类型参数的顺序不同:当函数包含多个不同类型的参数时,类型的先后排列顺序不同,比如一个先 int 后 double,另一个先 double 后 int。

函数重载的常见形式

接下来,我们通过 3 个具体的例子,分别看看编译器是如何根据参数个数、参数类型以及参数顺序来匹配重载函数的。

1. 参数个数不同

最直观的一种重载形式,就是函数名相同,但接收的参数个数不同。我们调用函数时,编译器只要数一数传入了几个实参,就能确定应该调用哪一个版本。

示例 1:按参数个数重载

#include <iostream>

// 计算 2 个整数之和
int getSum(int a, int b) {
    return a + b;
}

// 计算 3 个整数之和
int getSum(int a, int b, int c) {
    return a + b + c;
}

int main() {
    std::cout << getSum(10, 20) << '\n';
    std::cout << getSum(10, 20, 30) << '\n';
    return 0;
}

运行结果如下。

30
60

分析:

在这个例子中,我们定义了两个同名的 getSum() 函数。当我们调用 getSum(10, 20) 传入 2 个实参时,编译器会自动匹配双参数版本算出 30。当我们调用 getSum(10, 20, 30) 传入 3 个实参时,则自动匹配三参数版本算出 60。

2. 参数类型不同

如果多个同名函数的参数个数相同,但对应位置上的参数数据类型不同,同样可以构成函数重载。

示例 2:按参数类型重载

#include <iostream>

void printValue(int val) {
    std::cout << "整数:" << val << '\n';
}

void printValue(double val) {
    std::cout << "小数:" << val << '\n';
}

void printValue(char val) {
    std::cout << "字符:" << val << '\n';
}

int main() {
    printValue(10);
    printValue(3.14);
    printValue('A');
    return 0;
}

运行结果如下。

整数:10
小数:3.14
字符:A

分析:

当我们调用 printValue(10) 时,实参是 int 整数,编译器会自动匹配第 1 个版本。调用 printValue(3.14) 传入 double 小数时,会自动匹配第 2 个版本。调用 printValue('A') 传入字符时,则会匹配第 3 个版本。

顺便提一点,如果多个重载函数内部的计算代码完全一样,仅仅是参数和返回值的数据类型不同,后面我们还可以借助函数模板把它们合并成一份通用代码。

3. 不同类型参数的顺序不同

当函数包含多个不同类型的参数时,如果这些参数类型的先后排列顺序不同,也能构成函数重载。

示例 3:按参数类型顺序重载

#include <iostream>

// 先 int 后 double
void display(int a, double b) {
    std::cout << "先 int 后 double:" << a << ", " << b << '\n';
}

// 先 double 后 int
void display(double a, int b) {
    std::cout << "先 double 后 int:" << a << ", " << b << '\n';
}

int main() {
    display(10, 2.5);
    display(2.5, 10);
    return 0;
}

运行结果如下。

先 int 后 double:10, 2.5
先 double 后 int:2.5, 10

分析:

display(10, 2.5) 的实参类型依次为 int 和 double,因此匹配第 1 个版本。而 display(2.5, 10) 的实参类型依次为 double 和 int,因此匹配第 2 个版本。

这里小伙伴们要特别注意:必须是 “不同数据类型” 的先后顺序不同才算重载。如果两个参数都是 int,仅仅把形参名从 (int a, int b) 调换成 (int b, int a),参数类型列表依然相同,是不能构成重载的。

函数重载的注意事项

函数重载生效的前提是:编译器在每一次调用处,都能根据实参唯一确定应该调用哪一个版本。在实际写代码时,小伙伴们要注意避开以下两类报错情况。

1. 不能构成重载的情况

函数重载只看参数列表中的 “参数类型、个数与顺序”,以下 3 种情况都不能构成合法的重载,如下表所示。

不能构成重载的情况
对比情况 函数声明形式 编译器处理结果
仅形参名称不同 void fn(int a); 与 void fn(int b); 视为同一个函数
仅值传递加 const void fn(int a); 与 void fn(const int a); 视为同一个函数
仅返回类型不同 int fn(int a); 与 double fn(int a); 直接编译报错

在上面这 3 种情况中,初学者最容易踩坑的是 “仅返回类型不同”。如果两个函数的名字和参数列表完全一样,只有返回类型不同,放在同一作用域里就会直接引发编译报错:

int getValue(int num);
double getValue(int num);

为什么这样不行呢?因为如果我们在代码中单独写一句 getValue(10); 而不接收返回值,调用处只提供了一个 int 实参 10,编译器根本无法判断这句代码想调用的是返回 int 的版本还是返回 double 的版本。

此外,表格中的 “仅值传递加 const” 也不能构成重载。因为在普通值传递中,形参只是外部实参的一份 “复印件”,加 const 只是限制函数内部能不能改这份复印件,对外部调用者传参没有任何区别。如果同时定义这两个函数,就会触发重复定义报错。

2. 重载调用的二义性

有时候几个重载函数本身的定义是合法的,但当我们调用函数时,某一次传入的实参恰好能同时匹配上两个重载版本,编译器就会因为无法抉择而报二义性(歧义)错误。常见的主要有以下 2 种情况:

  • 类型转换引发歧义:比如对示例 3 的两个 display() 函数传入两个整数 display(10, 20)。此时两个版本都只需把其中一个 int 通过隐式类型转换变成 double 就能匹配,编译器无法抉择就会报错。
  • 与默认参数混用引发歧义:如果我们将函数重载与上一节学过的默认参数混在一起使用,很容易导致一次调用同时命中两个函数。

下面我们通过一个例子来看看重载与默认参数冲突时的报错表现。

示例 4:重载与默认参数冲突

#include <iostream>

void show(int val) {
    std::cout << val << '\n';
}

void show(int val, int mode = 1) {
    std::cout << val << ", " << mode << '\n';
}

int main() {
    show(10);
    return 0;
}

运行结果如下。

(报错)error: call of overloaded 'show(int)' is ambiguous

分析:

从报错信息中的 is ambiguous(存在歧义)可以看出,编译器不知道应该执行哪一个函数。

这是因为 show(10) 只传入了一个整数实参,既能直接匹配单参数的 show(int val),也能通过省略默认参数去匹配 show(int val, int mode = 1)。因此,当一个带默认参数的函数已经够用时,大家就不要再额外定义会起冲突的重载版本了。

函数重载的底层原理

为什么在 C 语言里不允许定义同名函数,而 C++ 却支持函数重载呢?

这是因为 C++ 编译器使用了一种叫做 “名字修饰(Name Mangling)” 的技术。在编译源代码时,编译器会把函数名和各个参数类型的缩写拼在一起,生成底层独一无二的内部名称。

比如 void printValue(int) 在底层可能被标记为 _Z10printValuei,而 void printValue(double) 会被标记为 _Z10printValued。虽然源码里它们都叫 printValue,但在编译器眼里其实是两个不同名的函数。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号