← 目录 / 第八章 · 函数 / 8.3 函数重载与内联函数
同一个函数名可以对应多种不同的参数组合吗?调用一个很小的函数,开销真的可以忽略不计吗?本节回答这两个问题——函数重载与内联函数。
8.3.1
假设你要写一个"求两个数中较大值"的函数,但数据类型可能是 int,也可能是 double,还可能需要比较三个数——总不能取名叫 MaxInt、MaxDouble、MaxThreeInt 这么多不同的名字吧?
函数重载(Function Overloading)允许在同一作用域内定义多个同名函数,只要它们的参数列表不同(参数的类型、个数或顺序不同),编译器就能把它们区分开,调用时自动匹配最合适的那一个。
| 1 | int Max(int a, int b) { return a > b ? a : b; } |
| 2 | double Max(double a, double b) { return a > b ? a : b; } |
| 3 | int Max(int a, int b, int c) { return Max(Max(a,b),c); } |
| 4 | |
| 5 | int main() |
| 6 | { |
| 7 | cout << Max(3, 5); // 调用①,结果 5 |
| 8 | cout << Max(3.5, 2.1); // 调用②,结果 3.5 |
| 9 | cout << Max(3, 5, 1); // 调用③,结果 5 |
| 10 | return 0; |
| 11 | } |
int GetValue(int x);double GetValue(int x); ← ❌ 编译错误:重复定义8.3.2
编译器选择重载版本的过程,可以拆成清晰的两步:
第一步——找出所有"可行函数":先看参数数量和类型,筛出所有"有可能匹配"的候选版本(数量不对的直接排除,类型能精确匹配或者能通过类型转换匹配上的,都算可行)。
第二步——从可行函数里选出"最佳匹配":在这些可行函数中,选择参数类型最贴近实参、需要的类型转换最少的那一个。如果隐式类型转换(见 2.2 节)还是没法让某个版本精确匹配,会优先选择转换幅度更小的版本。如果筛到最后,有两个或更多候选"一样好"、分不出谁更优,就会产生二义性错误。
以调用 Max(3, 5) 为例:
对照上面两步来看:Max(double, double) 和 Max(int, int) 因为参数数量都对得上,都算"可行函数";但 Max(int, int, int) 参数个数不对,第一步就被淘汰了。剩下两个可行函数里,Max(int, int) 不需要任何类型转换就能精确匹配,是"最佳匹配",因此被选中。如果编译器找不到任何一个"最合适"的版本——比如有两个候选函数经过转换后都同样匹配、难分高下——就会报调用二义性错误(ambiguous call),程序无法编译通过。
8.3.3
void F(int a, double b) 和 void F(double a, int b) 属于两个不同的重载(顺序不同),但和自己完全相同的签名再定义一遍就是重复定义。void F(int a) 与 void F(int a, int b = 5) 同时存在时,调用 F(10) 对编译器来说,两个函数都能满足(第二个函数省略 b 后就等于 F(10)),于是产生二义性错误,编译不通过。解决办法:要么去掉其中一个重载,要么调用时显式传够参数,避免两个候选同时"够格"。F(int) 和 F(double),调用 F('A') 时 char 既能转成 int 也能转成 double。(进阶细节:当实参类型没有精确匹配任何一个重载版本时,编译器会依据一套比较复杂的类型提升规则去挑选"更接近"的版本——比如这里会优先转成 int——这套规则不总是符合直觉,容易导致调用结果出乎意料。)最稳妥的做法是:调用时尽量保证实参类型能精确匹配某个重载版本,拿不准就用强制类型转换把意图写清楚。8.3.4
每次调用一个普通函数,程序都要经历"跳转到函数所在的位置 → 执行 → 再跳转回来"这一整套流程,这个过程本身是有开销的(涉及压栈、跳转、返回地址记录等)。对于函数体只有一两行、但被频繁调用的小函数,这份开销占比会变得不可忽视。
在函数定义前加上关键字 inline,就是在告诉编译器:请把每一处调用这个函数的地方,直接替换成函数体本身的代码,省去"真正跳转过去再跳回来"的过程。
| 1 | inline int Square(int x) // inline 写在返回类型前面 |
| 2 | { |
| 3 | return x * x; |
| 4 | } |
| 5 | |
| 6 | int main() |
| 7 | { |
| 8 | int y = Square(5) + 1; // 编译后可能直接变成 y = 5 * 5 + 1; |
| 9 | return 0; |
| 10 | } |
inline 关键字,只是请求编译器尝试内联展开,编译器有最终决定权——如果函数体太大、包含循环或递归,编译器很可能会直接忽略这个建议,仍按普通函数处理。反过来,即使不写 inline,现代编译器也可能自动把简单的小函数做内联优化。因此 inline 更多是一种"性能提示",而不是必须遵守的语法规则。-O2 优化)通常已经足够聪明,会自动内联合适的小函数,手动加 inline 关键字更多是一种"锦上添花"的提示,而不是必需操作。理解它的原理比死记写法更重要。inline 的函数在多个 .cpp 文件里都 #include 了同一个定义了它的头文件,链接时会报"重复定义"的错误;而 inline 函数被特许可以在多个文件里重复出现相同的定义而不报错,这也是为什么很多小型工具函数会写成 inline 并直接放在头文件中。这是一个更偏工程实践的知识点,竞赛中通常用不到(竞赛一般只有一个文件),了解即可。