C++ 语言重载运算符
当运算符被用于类类型的对象时,C++ 语言允许我们为其指定新的含义,和内置类型的转换一样,类类型转换隐式地将一种类型的对象转换成另一种我们所需类型的对象。
1. 基本概念
重载的运算符是具有特殊名字的函数:它们的名字由关键字 operator 和其后要定义的运算符号共同组成。同其他函数一样,重载的运算符也包含返回类型、参数列表以及函数体。
重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个。对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。除了重载的函数调用运算符 operator() 之外,其他重载运算符不能含有默认实参。
如果一个运算符函数是成员函数,则它的第一个 (左侧) 运算对象绑定到隐式的 this 指针上,成员运算符函数的 (显式) 参数数量比运算符的运算对象总数少一个。当一个重载的运算符是成员函数时,this 绑定到左侧运算对象。成员运算符函数的 (显式) 参数数量比运算对象的数量少一个。
对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数。这一约定意味着当运算符作用于内置类型的运算对象时,我们无法改变该运算符的含义。我们可以重载大多数 (但不是全部) 运算符。
// 错误:不能为 int 重定义内置的运算符
int operator+(int, int);
有四个符号 (+ 、- 、* 、& ) 既是一元运算符也是二元运算符,所有这些运算符都能被重载,从参数的数量我们可以推断到底定义的是哪种运算符。对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。不考虑运算对象类型的话,x == y + z 永远等价于 x == (y + z) 。
1.1 直接调用一个重载的运算符函数
通常情况下,我们将运算符作用于类型正确的实参,从而以这种间接方式调用 重载的运算符函数。我们也能像调用普通函数一样直接调用运算符函数,先指定函数名字,然后传入数量正确、类型适当的实参:
// equivalent calls to a nonmember operator function - 一个非成员运算符函数的等价调用
data1 + data2; // normal expression - 普通的表达式
operator+(data1, data2); // equivalent function call - 等价的函数调用
这两次调用是等价的,它们都调用了非成员函数 operator+ ,传入 data1 作为第一个实参、传入 data2 作为第二个实参。
我们像调用其他成员函数一样显式地调用成员运算符函数。具体做法是,首先指定运行函数的对象 (或指针) 的名字,然后使用点运算符 (或箭头运算符) 访问希望调用的函数:
data1 += data2; // expression-based call - 基于调用的表达式
data1.operator+=(data2); // equivalent call to a member operator function - 对成员运算符函数的等价调用
这两条语句都调用了成员函数 operator+= ,将 this 绑定到 data1 的地址、将 data2 作为实参传入了函数。
1.2 某些运算符不应该被重载
使用重载的运算符本质上是一次函数调用,所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上。特别是,逻辑与运算符、逻辑或运算符和逗号运算符的运算对象求值顺序规则无法保留下来。除此之外,&& 和 || 运算符的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。因为上述运算符的重载版本无法保留求值顺序和/或短路求值属性,因此不建议重载它们。当代码使用了这些运算符的重载版本时,用户可能会突然发现他们一直习惯的求值规则不再适用了。
我们一般不重载逗号运算符和取地址运算符:C++ 语言已经定义了这两种运算符用于类类型对象时的特殊含义,这一点与大多数运算符都不相同。因为这两种运算符已经有了内置的含义,所以一般来说它们不应该被重载,否则它们的行为将异于常态,从而导致类的用户无法适应。通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符。
1.3 使用与内置类型一致的含义
如果某些操作在逻辑上与运算符相关,则它们适合于定义成重载的运算符:
- 如果类执行
IO 操作,则定义移位运算符使其与内置类型的 IO 保持一致。 - 如果类的某个操作是检查相等性,则定义
operator== 。如果类有了 operator== ,意味着它通常也应该有operator!= 。 - 如果类包含一个内在的单序比较操作,则定义
operator< 。如果类有了 operator< ,则它也应该含有其他关系操作。 - 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容:逻辑运算符和关系运算符应该返回
bool ,算术运算符应该返回一个类类型的值,赋值运算符和复合赋值运算符则应该返回左侧运算对象的一个引用。
1.4 尽量明智地使用运算符重载
每个运算符在用于内置类型时都有比较明确的含义。以二元 + 运算符为例,它明显执行的是加法操作。把二元 + 运算符映射到类类型的一个类似操作上可以极大地简化记忆。
当在内置的运算符和我们自己的操作之间存在逻辑映射关系时,运算符重载的效果最好。使用重载的运算符显然比另起一个名字更自然也更直观。过分滥用运算符重载也会使我们的类变得难以理解。
在实际编程过程中,一般没有特别明显的滥用运算符重载的情况。只有当操作的含义对于用户来说清晰明了时才使用运算符。如果用户对运算符可能有几种不同的理解,则使用这样的运算符将产生二义性。
1.5 赋值和复合赋值运算符
赋值运算符的行为与复合版本的类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用。重载的赋值运算应该继承而非违背其内置版本的含义。
如果类含有算术运算符或者位运算符,则最好也提供对应的复合赋值运算符。+= 运算符的行为显然应该与其内 置版本一致,即先执行 + ,再执行 = 。
1.6 Choosing Member or Nonmember Implementation - 选择作为成员或者非成员
- 赋值 (
= )、下标 ([] )、调用 (() ) 和成员访问箭头 (-> ) 运算符必须是成员函数。 - 复合赋值运算符一般来说应该是成员函数,但并非必须,这一点与赋值运算符略有不同。
- 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员函数。
- 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。
我们能求一个 int 和一个 double 的和,因为它们中的任意一个都可以是左侧运算对象或右侧运算对象,所以加法是对称的。如果我们想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。
当我们把运算符定义为成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。
std::string s = "yongqiang";
std::string t = s + "!"; // 我们能把一个 const char* 加到一个 std::string 对象中。
std::string u = "cheng" + s; // 如果 + 是 std::string 的成员,则产生错误。
如果 operator+ 是 std::string 类的成员,则上面的第一个加法等价于 s.operator+("!") 。同样的,"cheng" + s 等价于 ”cheng”.operator+(s) 。显然 “cheng” 的类型是 const char* 。这是一种内置类型,根本就没有成员函数。因为 std::string 将 + 定义成了普通的非成员函数,所以 "cheng" + s 等价于 operator+("cheng", s) 。和任何其他函数调用一样,每个实参都能被转换成形参类型。唯一的要求是至少有一个运算对象是类类型,并且两个运算对象都能准确无误地转换成 std::string 。
References
(美) Stanley B. Lippman, (美) Josée Lajoie, (美) Barbara E. Moo 著, 王刚, 杨巨峰 译. C++ Primer 中文版[M]. 第 5 版. 电子工业出版社, 2013. https://www.informit.com/store/c-plus-plus-primer-9780321714114
|