在 《函数重载简介》课程中,你已学习了函数重载:只要同名函数的函数原型各不相同,编译器就能在调用时解析到正确的版本。借此,你可以为不同数据类型编写同名函数的不同实现,而无需为每种类型绞尽脑汁地起新名字。
在 C++ 中,运算符本质上就是函数。通过对“运算符函数”应用函数重载,你可以为现有运算符定义自己的版本,使之能够作用于自定义类型(包括你编写的类)。利用函数重载来实现这一机制,就称为运算符重载。
本章将深入探讨与运算符重载相关的主题。
运算符即函数
先看下面的例子:
int x{ 2 };
int y{ 3 };
std::cout << x + y << '\n';
编译器自带一个用于整数操作数的内置加号运算符(+)。该函数将整数 x
与 y
相加并返回整数结果。当你写下表达式 x + y
时,可将其视为函数调用 operator+(x, y)
(其中 operator+
是函数名)。
再看一段类似代码:
double z{ 2.0 };
double w{ 3.0 };
std::cout << w + z << '\n';
编译器同样提供了用于 double
操作数的加号运算符内置版本。表达式 w + z
会被转换为函数调用 operator+(w, z)
;通过函数重载,编译器决定调用 double
版本而非 int
版本。
现在考虑对自定义类对象使用加法:
Mystring string1{ "Hello, " };
Mystring string2{ "World!" };
std::cout << string1 + string2 << '\n';
直觉上,我们希望屏幕输出 "Hello, World!"
。然而,Mystring
是用户自定义类型,编译器并未内置对应版本的加号运算符,于是会报错。为了让表达式按预期工作,我们需要编写一个重载函数,告诉编译器当两个操作数均为 Mystring
类型时,加号应如何运算。下一课程将介绍具体做法。
重载运算符的决议规则
当编译器求值含运算符的表达式时,遵循以下规则:
- 若所有操作数均为基本数据类型,且有对应内建例程,则调用之;否则产生编译错误。
- 若任一操作数为用户自定义类型(如你的类或枚举),编译器使用函数重载决议算法(参见 11.3《函数重载决议与二义性匹配》)寻找最佳匹配的重载运算符。
- 这可能涉及将一个或多个操作数隐式转换为重载运算符所需的形参类型。
- 也可能通过稍后本章将讲到的重载类型转换,把自定义类型隐式转换为基本类型,以匹配内建运算符。
- 若找不到匹配(或存在二义性),编译器报错。
运算符重载的限制
C++ 中几乎所有现有运算符都可重载,例外包括: 条件运算符
?:
、sizeof
、作用域解析符::
、成员访问符.
、成员指针访问符.*
、typeid
以及各类型转换运算符。只能重载已存在的运算符,不能创建新运算符或给旧运算符改名。例如,不能定义
operator**
表示幂运算。重载运算符的至少一个操作数必须是用户自定义类型。
- 因而可以重载
operator+(int, Mystring)
,但不能重载operator+(int, double)
。 - 由于标准库类型亦被视为用户自定义,可重载
operator+(double, std::string)
,但这并非好做法,因为未来标准可能定义该重载,导致代码失效。 - 最佳实践:重载运算符应至少作用于一个程序自定义类型,以避免未来标准升级时产生冲突。
- 因而可以重载
无法改变运算符所支持的操作数个数。
所有运算符保持默认优先级与结合性,不可更改。
优先级与结合性的陷阱
某些初学者试图用按位异或运算符 ^
实现幂运算。但在 C++ 中,operator^
的优先级低于基本算术运算符,导致表达式求值不符合数学直觉。
数学上: 4 + 3² = 4 + 9 = 13
而在 C++: 4 + 3 ^ 2 解释为 (4 + 3) ^ 2 = 7 ^ 2 = 49
每次都必须显式加括号 4 + (3 ^ 2)
才能得到正确结果,既不直观又易出错。因此,应按运算符原始语义类比使用。
最佳实践 重载运算符时,应尽量保持其功能与原始语义一致。 若重载含义不够清晰直观,请改用具名函数。
返回值约定
- 不修改操作数的运算符(如算术运算符)通常应按值返回结果。
- 修改最左操作数的运算符(如前置
++
、赋值运算符)通常应按引用返回最左操作数。
最佳实践 不修改操作数的运算符按值返回结果; 修改最左操作数的运算符按引用返回最左操作数。
结语
在上述约束之内,运算符重载仍能为自定义类提供丰富而直观的功能。你可以重载 +
来连接自定义字符串类,或将两个 Fraction
对象相加;重载 <<
以方便地把类对象输出到屏幕或文件;重载 ==
来比较两个类对象。正因如此,运算符重载成为 C++ 最有用的特性之一——它让你能以更自然、更符合直觉的方式操作自定义类型。
后续课程中,我们将深入探讨各类运算符的重载方法。