12.3 双目运算符
21天学通C++(第7版)
对两个操作数进行操作的运算符称为双目运算符。以全局函数或静态成员函数的方式实现的双目运算符的定义如下:
以类成员的方式实现的双目运算符的定义如下:
以类成员的方式实现的双目运算符只接受一个参数,其原因是第二个参数通常是从类属性获得的。
12.3.1 双目运算符的类型
表12.2列出了可在C++应用程序中重载或重新定义的双目运算符。
12.3.2 双目加法与双目减法运算符
与递增/递减运算符类似,如果类实现了双目加法和双目减法运算符,便可将其对象加上或减去指定类型的值。再来看看日历类Date,虽然前面实现了将Date递增以便前移一天的功能,但它还不支持增加5天的功能。为实现这种功能,需要实现双目加法运算符,如程序清单12.5中的代码所示。
程序清单12.5 实现了双目加法运算符的日历类
输出:
分析:
第14~25行是双目运算符+和-的实现,让您能够使用简单的加法和减法语法,如main()中的第41和45行所示。
对字符串类来说,双目加法运算符也很有用。第9章分析了简单的字符串包装类MyString,它封装了一个C风格字符串,并提供了内存管理、复制等功能,如程序清单9.9所示。但这个类不支持使用如下语法将两个字符串拼接起来:
不用说,实现运算符+后,MyString使用起来将非常容易,值得去实现它:
程序清单9.9中添加上述代码,并提供实现为空的私有默认构造函数MyString()后,便可使用加法语法了。本章后面的程序清单12.12提供了一个MyString类,它实现了+等运算符。
运算符提高了类的可用性,但实现的运算符必须合理。对于Date类,您实现了加法和减法运算符,但对于MyString类,只实现了加法运算符(+)。这是因为对字符串执行减法运算的可能性极少,实现这样的运算符很可能是在浪费时间。
12.3.3 实现运算符+=与-=
加并赋值运算符支持语法a+=b;,这让程序员可将对象a增加b。这样,程序员可重载加并赋值运算符,使其接受不同类型的参数b。程序清单12.6让您能够给Date对象加上一个整数。
程序清单12.6 定义运算符+=和-=,以便将日历向前或向后翻整型输入参数指定的天数
输出:
分析:
运算符+=和-=是在第14~24行定义的。这些运算符让您能够加上或减去指定的天数,如main()中的下述代码所示:
运算符+=和-=接受一个int参数,让您能够给Date对象加上或减去指定的天数,就像处理的是整数一样。您还可提供运算符+=的重载版本,让它接受一个虚构的CDays对象作为参数:
乘并赋值运算符(*=)、除并赋值运算符(/=)、求模并赋值运算符(%=)、减并赋值运算符(-=)、左移并赋值运算符(<<=)、右移并赋值运算符(>>=)、异或并赋值运算符(^=)、按位或并赋值运算符(|=)以及按位与并赋值运算符(&=)的语法都与程序清单12.6所示的加并赋值运算符类似。
虽然重载运算符的最终目标是让类更直观,更易于使用,但很多时候实现这些运算符并没有意义。例如,前面的日历类Date绝对不会用到按位与并赋值运算符&=。这个类的用户应该不会想通过greatDay &= 20;等操作获得有用的结果。
12.3.4 重载等于运算符(==)和不等运算符(!=)
如果像下面这样将两个Date对象进行比较,结果将如何呢?
由于还没有定义等于运算符,编译器将对这两个对象进行二进制比较,并仅当它们完全相同时才返回true。在有些情况下(包括现在的Date类),这是可行的。然而,如果类有一个非静态字符串成员,它包含字符串值(char *),如程序清单9.9所示的MyString,则比较结果可能不符合预期。在这种情况下,对成员属性进行二进制比较时,实际上将比较字符串指针,而字符串指针并不相等(即使指向的内容相同),因此总是返回false。
因此,正确的做法是定义比较运算符。等于运算符的通用实现如下:
实现不等运算符时,可重用等于运算符:
不等运算符的结果与等于运算符相反(逻辑非)。程序清单12.7列出了日历类Date定义的比较运算符。
程序清单12.7 运算符==和!=
输出:
分析:
等于运算符(==)的实现很简单,它在年、月、日都相同时返回true,如第14~19行所示。实现不等运算符时,重用了等于运算符的代码,如第23行所示。有了这两个运算符后,就可对两个Date对象(Holiday1和Holiday2)进行比较了,如main()中的第42和47行所示。
12.3.5 重载运算符<、>、<=和>=
程序清单12.7所示的代码让Date类足够聪明,能够判断两个Date对象是否相等。然而,如果要使用该类执行类似下面的条件检查,该如何办呢?
如果能够使用这个日历类来比较两个日期,确定哪个在前、哪个在后,将很有用。编写这类的程序员应实现这种比较,让这个类对用户来说尽可能友好和直观,如程序清单12.8所示。
程序清单12.8 实现运算符<、>、<=和>=
输出:
分析:
这里要讨论的运算符是在第21~52行实现的。注意到实现这些运算符时,重用了其他运算符的代码。
在main()函数的第75~84行,使用了这些运算符,以演示这些运算符使得使用Date类简单而直观。
12.3.6 重载复制赋值运算符(=)
有时候,需要将一个类实例的内容赋给另一个类实例,如下所示:
如果您没有提供复制赋值运算符,这将调用编译器自动给类添加的默认复制赋值运算符。根据类的特征,默认复制赋值运算符可能不可行,具体地说是它不复制类管理的资源。与复制构造函数一样,为确保进行深复制,您需要提供复制赋值运算符:
如果类封装了原始指针,如程序清单9.9所示的MyString类,则确保进行深复制很重要。如果没有实现赋值运算符,编译器将提供默认的复制赋值运算符,但它只复制char* Buffer包含的地址,而不复制指向的内存中的内容。这与没有提供复制构造函数时出现的情况相同。为确保赋值时进行深复制,应定义复制赋值运算符,如程序清单12.9所示。
程序清单12.9 对程序清单9.9所示的MyString类进行改进,添加了复制赋值运算符
输出:
分析:
在这个示例中,笔者故意省略了复制构造函数,旨在减少代码行(但您编写这样的类时,应添加它,详情请参阅程序清单9.9)。复制赋值运算符是在第25~39行实现的,其功能与复制构造函数很像。它首先检查源和目标是否同一个对象。如果不是,则释放成员Buffer占用的内存,再重新给它分配足以存储复制源中文本的内存,然后使用strcpy()进行复制,如第36行所示。
相比于程序清单9.9,程序清单12.9的另一个细微差别在于,使用返回const char*的转换运算符替代了函数GetString(),如第53~56行所示。该运算符让MyString类使用起来更容易,如第68行所示——使用一条cout语句显示了两个MyString实例的内容。
如果您编写的类管理着动态分配的资源(如C风格字符串char*)、动态数组等,除构造函数和析构函数外,请务必实现复制构造函数和复制赋值运算符。
如果没有考虑对象被复制时出现的资源所有权问题,您的类就是不完整的,使用时甚至会有危险。
要创建不允许复制的类,可将复制构造函数和复制赋值运算符都声明为私有的。只需这样声明(甚至都不用提供实现)就足以让编译器在遇到试图复制对象(将对象按值传递给函数或将一个对象赋给另一个对象)的代码时引发错误。
12.3.7 下标运算符
下标运算符让您能够像访问数组那样访问类,其典型语法如下:
编写封装了动态数组的类(如封装了char* Buffer的MyString)时,通过实现下标运算符,可轻松地随机访问缓冲区中的各个字符:
程序清单12.10是一个简单的示例,演示了下标运算符([])让用户能够使用常规数组语法来遍历MyString实例包含的字符。
程序清单12.10 在MyString类中实现下标运算符,以便随机访问MyString::Buffer包含的字符
输出:
分析:
这个程序很有趣,它接受用户输入的句子,并使用它创建一个MyString对象,如第61行所示;接下来,在一个for循环中,使用下标运算符([])和数组语法逐字符地打印该字符串,如第64~65行所示。下标运算符([])是在第31~35行实现的,它首先确保指定的位置没有超出char*Buffer末尾,然后返回指定位置处的字符。
实现运算符时,应使用关键字const,这很重要。在程序清单12.10中,将下标运算符([])的返回类型声明成了const char&。即便没有关键字const,该程序也能通过编译。这里使用它旨在禁止使用下面这样的代码:
通过使用const,可禁止从外部通过运算符[]直接修改成员MyString::Buffer。除将返回类型声明为const外,还将该运算符的函数类型设置成为const,这将禁止该运算符修改类的成员属性。
一般而言,应尽可能使用const,以免无意间修改数据,并最大限度地保护类的成员属性。
实现下标运算符时,可在程序清单12.10所示版本的基础上进行改进。这个版本只实现了一个下标运算符,它可用于读写动态数组的元素。
然而,也可实现两个下标运算符,其中一个为const函数,另一个为非const函数:
编译器很聪明,能够在读取MyString对象时调用const函数,而在对MyString执行写入操作时调用非const函数。因此,如果愿意,可在两个下标函数中实现不同的功能。例如,一个运算符记录对容器的写入操作,而另一个记录对容器的读取操作。还有其他双目运算符可被重定义或重载(如表12.2所示),但本章不打算介绍它们。这些运算符的实现与已讨论的运算符类似。
如果其他运算符(如逻辑运算符和按位运算符)有助于改善您编写的类,就应实现它们。显然,诸如Date等日历类没有必要实现逻辑运算符,但处理字符串和数字的类可能需要实现它们。
应根据类的目标和用途重载运算符或实现新的运算符。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。