Menu

C++ 函数重载:同名函数,不同参数

C++ 函数重载让多个函数共用一个名字,只要它们的参数列表不同。学习重载决议如何选出匹配的版本、为什么仅返回类型不同不算重载,以及要避开的二义性和默认实参陷阱。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

一个名字,多个版本

你经常需要对不同种类的数据做同一种操作:打印一个 int,打印一个 string,打印一个 double。在某些语言里,你得发明 printInt、printString、printDouble 这样的名字。C++ 允许你给它们都起同一个名字,再通过参数来区分它们。这就是函数重载。

规则很简单:多个函数可以共用一个名字,只要它们的参数列表不同,也就是参数个数、类型或顺序不同。编译器会查看每个调用处的实参,替你选出匹配的版本。

三个函数,一个名字。每次调用都会落到参数类型与实参相符的那个版本上。正是这一点让 std::cout << x 对 int、double 和字符串都能工作:operator<< 被重载了很多次。

怎样才算不同的重载

区分重载的只有参数列表。你可以改变:

int  area(int side);                 // 1 param
int  area(int width, int height);    // 2 params  -> different
double area(double r);               // different type -> different

void log(string msg, int level);     // order matters...
void log(int level, string msg);     // ...so this is different too

上面每一个都是合法且不同的重载。编译器先从所有名为 area 的函数中建立候选集,再按实参的个数和类型进行匹配。

只有返回类型不同是不够的

几乎每个人都会掉进这个坑:不能仅凭返回类型来重载。返回值在选择重载时不起任何作用,因为编译器是根据实参来决定调用哪个函数的,在它看返回什么之前就已经决定了。

int    convert(double x);   // OK
double convert(double x);   // ERROR: redefinition - only the return type differs

这无法编译。如果参数列表完全相同,那么就重载而言,这两个声明就是同一个函数,你会得到一个重定义错误。如果想根据结果类型来区分,就修改某个参数(或者改用模板,或在调用处做类型转换)。

重载决议如何选出赢家

当你发起一次调用时,编译器会给每个可行的重载排名,然后选出最佳匹配。大致来说,它按以下顺序优先:

  1. 精确匹配(不需要任何转换)。
  2. 提升(例如 char 或 short -> int,float -> double)。
  3. 标准转换(例如 int -> double,double -> int,指向基类的指针)。

如果恰好有一个重载严格优于其他所有重载,它就胜出。看看精确匹配如何胜过转换:

'A' 是 char,但提升为 int 的排名高于转换为 double,所以调用的是 int 重载。正是这些排名规则让重载决议通常都能“自然地做对”,偶尔也会让你吃一惊。

二义性陷阱

如果两个重载匹配得一样好,谁也不比谁更好,编译器就拒绝猜测,报告调用有二义性(ambiguous call)。教科书式的例子是两个重载各自都需要一次同等级别的转换:

void f(int x);
void f(double x);

f(0L);   // ERROR: ambiguous - long -> int and long -> double are equal-rank conversions

int 和 double 都不是 long 的精确匹配,而两种转换又处在同一级别,所以这个调用有二义性。你有两种干净的修复方法:

一个相关的意外是传入字符串字面量。void g(const string&) 和 void g(bool) 都会争夺 g("hi"),而且 bool 可能会胜出,因为把 const char* 转换成 bool(非空 -> true)比构造一个 std::string 所需的步骤更少。如果你看到字符串字面量莫名其妙地调用了你的 bool 重载,原因就在这里:添加一个 const char* 或 const string& 重载,让它拿下精确匹配。

重载和默认实参不宜混用

默认实参不能代替重载,而把两者结合起来还会产生二义性。它们都能响应同一个调用,于是编译器无法选择:

void connect(string host, int port = 8080);   // can be called with 1 arg
void connect(string host);                     // also callable with 1 arg

connect("localhost");   // ERROR: ambiguous - both match a single argument

对于同一种调用形式,只选一种做法。如果行为完全相同,只是想让某些参数可选,就用默认实参;如果不同的实参列表应该运行真正不同的代码,就用重载。把两者混在一起,导致两个签名在相同的实参个数下发生冲突,必然会得到二义性错误。

还有一个值得弄清楚的区别:重载不是重写(override)。重载在编译期,在同一作用域中名字相同、参数不同的函数之间做出决议。重写则是在运行时由派生类替换一个 virtual 函数,要求签名完全相同,这是后面虚函数要讲的主题。

下一步:Lambda 表达式

重载让一个名字拥有多个按类型区分、在编译期选定的实现。不过有时你根本不想要一个有名字的函数,而是需要一个就在使用处定义的小小的一次性函数,常常是为了传给 sort 这样的算法。这正是 lambda 表达式:可以内联编写、能捕获周围变量、在一个表达式里就能传出去的匿名函数。接下来我们会看看怎样编写它们,以及什么时候它们比一个完整的具名函数更好用。

常见问题

C++ 中的函数重载是什么?

函数重载让你定义多个同名函数,只要它们的参数列表不同(参数个数、类型或顺序不同)。编译器根据你传入的实参选择调用哪一个,所以 print(42) 和 print("hi") 可以调用两个不同的 print 函数。

两个 C++ 函数能只在返回类型上不同吗?

不能。重载必须在参数列表上有所不同。int f(int) 和 double f(int) 会导致编译错误:返回类型不属于重载决议所用的签名,因为编译器是根据调用处的实参来选择重载的,在用到返回值之前就已经做出了选择。

重载函数为什么会出现“ambiguous call”(调用有二义性)错误?

当两个重载匹配得一样好、编译器无法偏向其中一个时,就会出现这个错误。经典的例子是 f(int) 和 f(double) 被 f(0L)(一个 long)调用,两者都需要同等级别的转换。解决办法是添加一个精确匹配的重载,或者把实参转换成你想要的类型。

Coddy 编程语言插图

和 Coddy 一起学编程

开始