在课程11.1 — 函数重载简介中,你了解了函数重载,它提供了一种机制,用于创建和解析对同名但具有唯一函数原型的多个函数的函数调用。这允许你创建函数的变体,以处理不同的数据类型,而无需为每个变体想出唯一的名称。
在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';
编译器还内置了针对双精度浮点数操作数的加号运算符(+)版本。表达式w + z变为函数调用operator+(w, z)
,并且使用函数重载来确定编译器应该调用此函数的双精度浮点数版本而不是整数版本。
现在考虑如果我们尝试添加两个程序定义的类对象会发生什么
Mystring string1 { "Hello, " };
Mystring string2 { "World!" };
std::cout << string1 + string2 << '\n';
在这种情况下,你期望会发生什么?直观的预期结果是字符串“Hello, World!”会打印到屏幕上。然而,由于Mystring是一个程序定义的类型,编译器没有内置的加号运算符版本可以用于Mystring操作数。因此在这种情况下,它会给我们一个错误。为了使其按我们想要的方式工作,我们需要编写一个重载函数来告诉编译器+运算符应该如何与两个Mystring类型的操作数一起工作。我们将在下一课中介绍如何做到这一点。
解析重载运算符
当计算包含运算符的表达式时,编译器使用以下规则
- 如果**所有**操作数都是基本数据类型,如果存在内置例程,编译器将调用它。如果不存在,编译器将产生编译错误。
- 如果**任何**操作数是程序定义类型(例如,你的某个类,或枚举类型),编译器将使用函数重载解析算法(在课程11.3 — 函数重载解析和歧义匹配中描述)来查看是否能找到一个明确的最佳匹配的重载运算符。这可能涉及隐式转换一个或多个操作数以匹配重载运算符的参数类型。它还可能涉及将程序定义类型隐式转换为基本类型(通过重载类型转换,我们将在本章后面介绍),以便匹配内置运算符。如果找不到匹配项(或找到歧义匹配项),编译器将报错。
运算符重载有哪些限制?
首先,C++中几乎任何现有运算符都可以重载。例外包括:条件运算符(?:)、sizeof、作用域运算符(::)、成员选择运算符(.)、指针成员选择运算符(.*)、typeid和类型转换运算符。
其次,你只能重载已存在的运算符。你不能创建新运算符或重命名现有运算符。例如,你不能创建operator**
来进行幂运算。
第三,重载运算符中至少有一个操作数必须是用户定义类型。这意味着你可以重载operator+(int, Mystring)
,但不能重载operator+(int, double)
。
由于标准库类被视为用户定义的,这意味着你可以定义operator+(double, std::string)
。然而,这不是一个好主意,因为未来的语言标准可能会定义这个重载,这可能会破坏任何使用了你的重载的程序。因此,最佳实践是你的重载运算符应该至少操作一个程序定义类型。这可以保证未来的语言标准不会潜在地破坏你的程序。
最佳实践
重载运算符应至少操作一个程序定义类型(作为函数的参数或隐式对象)。
第四,不可能改变运算符支持的操作数数量。
最后,所有运算符都保持其默认的优先级和结合性(无论它们用于何种目的),并且不能更改。
一些新程序员尝试重载按位异或运算符(^)来进行幂运算。然而,在C++中,运算符^的优先级低于基本算术运算符,这会导致表达式求值不正确。
在基本数学中,幂运算在基本算术运算之前进行,所以4 + 3 ^ 2解析为4 + (3 ^ 2) => 4 + 9 => 13。
然而,在C++中,算术运算符的优先级高于运算符^,所以4 + 3 ^ 2解析为(4 + 3) ^ 2 => 7 ^ 2 => 49。
你需要每次使用时都显式地用括号括住指数部分(例如4 + (3 ^ 2)),才能使其正常工作,这不直观,并且容易出错。
由于这个优先级问题,通常最好只以与运算符原始意图类似的方式使用运算符。
最佳实践
重载运算符时,最好使运算符的功能尽可能接近运算符的原始意图。
此外,由于运算符没有描述性名称,它们的目的并非总是清晰明了。例如,对于字符串类,operator+可能是连接字符串的合理选择。但是operator-呢?你期望它做什么?这不清楚。
最佳实践
如果重载运算符的含义不明确或不直观,请改用命名函数。
最后,重载运算符应以与原始运算符一致的方式返回值。不修改其操作数(例如算术运算符)的运算符通常应按值返回结果。修改其最左侧操作数(例如前置自增、任何赋值运算符)的运算符通常应按引用返回最左侧操作数。
最佳实践
不修改其操作数(例如算术运算符)的运算符通常应按值返回结果。
修改其最左侧操作数(例如前置自增、任何赋值运算符)的运算符通常应按引用返回最左侧操作数。
在这些限制下,你仍然会发现许多有用的功能可以为你的自定义类进行重载!你可以重载+运算符来连接你的程序定义字符串类,或将两个Fraction类对象相加。你可以重载<<运算符,使其易于将你的类打印到屏幕(或文件)。你可以重载相等运算符(==)来比较两个类对象。这使得运算符重载成为C++中最有用的特性之一——仅仅因为它允许你以更直观的方式使用你的类。
在接下来的课程中,我们将更深入地探讨不同类型运算符的重载。