C++抽象编程·运算符重载与友元函数

2023-02-12,,,,

运算符重载(Operator overloading)

从我们在几个前篇的类的层次介绍中可以知道,C++可以扩展标准运算符,使其适用于新类型。这种技术称为运算符重载。 例如,字符串类重载+运算符,使其在应用于字符串时的行为会有所不同。 当C++编译器看到 + 运算符时,它通过查看操作数的类型来决定使用的是哪种运算,如果编译器看到 + 应用于两个整数,它会采用我们一般的数字相加。 如果操作数是字符串,那么实现的就是两个字符串之间的连接。
重载运算符的能力是C++的一个强大功能,可以使程序更容易阅读,但只有当每个运算符在其应用类型之间保持一致时才能应用。例如,重载+运算符的类用于概念上类似于加法的操作,例如连接字符串。如果你写一个表达式:

s1 + s2;
1
1
对于类型字符串的两个变量,很容易将此操作视为将字符串添加到一起的操作。但是,如果重新定义一个操作符,那么会使得读者不知道它是什么意思,这时操作符重载会使程序基本上无法读取。因此,我们应该把这种技术限制在只有在提高程序可读性的情况下才能使用此功能。

重载插入运算符(Overloading the insertion operator)

从之前的中的point.h接口可以看出,Point类导出一个名为toString的方法,它将Point对象转换为包含括号中的坐标值的字符串。包括这种方法的主要目的是使得容易显示一个Point的值。调试程序时,显示变量的值通常很方便。 要显示名为pt的Point变量的值,我们需要做的就是将一个语句添加到程序中,如下所示:

cout << "pt = " << pt.toString() << endl;
1
1
运算符重载使得进一步简化此过程成为可能。 C++已经重载流插入运算符<<,以便它可以显示字符串以及原始类型。但是如果我们括重载此运算符以支持Point类,那么可以简化前面的语句为:

cout << "pt = " << pt << endl;
1
1
毫无疑问,这里有一些小小的变化,但是对比上面的那句话来说,我们却是不用为输出思考那么多。
C++中的每个运算符与用于定义其重载行为的函数名相关联。函数名称由关键字 operator 组成,后跟运算符符号。 例如,如果要为某些新类型重新定义 + 运算符,则可以定义一个名为operator +的函数,该函数接受该类型的参数.同样,可以通过为函数运算符<<提供新的定义来重载插入运算符。

编写运算符 << 函数的最难的部分是编写它的原型。<< 操作符的左操作数是输出流。对于这个参数,选择实现插入运算符的最通用类——ostream。 << 的右侧操作数是要插入该流的值,在此示例中为Point对象。运算符<<的重载定义因此有两个参数:一个ostream和一个Point。

ostream & operator<=(ostream & os, Point pt);
1
1
通过引用返回结果

但是,完成原型需要谨慎考虑流不能值传递的事实,不然函数就没了意义。这个限制意味着ostream参数必须通过引用传递。 但是,当 << 运算符返回时也会出现同样的限制。插入运算符通过返回输出流实现其美妙的连接行为(就是把输出的东西连在一起输出,例如 cout << “hello” <<”world”,虽然输出的都是一个单词,但是两个 << 运算符就是把这个值存入流,然后将其转发到链中的下一个<<运算符。从而完成链接) 为了避免在流程的这一端复制流,运算符<<的定义也必须通过引用返回其结果。
通过引用返回结果比通过引用传递参数更少见。 也就有几种情况,这里面就包括当前重载我们运算符的示例,但没有必要将其作为一般工具来介绍。对于那些出现的应用程序,我们只要知道可以通过引用指定返回值,就可以使用与引用的函数相同的语法:我们只需在结果类型之后添加一个&符号即可。因此我们可以这样理解这个代码:

ostream & operator<=(ostream & os, Point pt);
1
1
将函数operator<= 中的返回的值,以引用的方式传递到输出流 ostream中。而且通过引用返回流,我们菜可以在周围的上下文中再次使用。所以,我们可以通过下列代码来重载我们的 << 运算符。

ostream & operator<<(ostream & os, Point pt) {
return os << pt.toString();
}
1
2
3
1
2
3
其他功能的重载

那么,既然可以重载插入运算符,那么我们是不是也可以重载其他类型的运算符呢?答案是肯定的。我们给Point类设计一些其他运算符。例如,给定两个点p1和p2,您可以通过应用 == 运算符来测试这些点是否相等,就像使用字符串或者数值一样。
这里C++提供了两种用于重载内置操作符的策略,以便它与新定义的类的对象一起工作。

你可以将运算符定义为类中的一种方法。 当您使用此样式重载运算符时,左操作数是接收器对象,右操作数作为参数传递。(就像上面的例子)
你可以将运算符定义为类外的自由函数。 如果使用此样式,则运算符的操作数都将作为参数传递
基于方法的运算符重载

如果使用基于(1)的样式,则扩展==操作符的第一步是将此运算符的原型添加到point.h接口,如下所示:

bool operator==(Point rhs);
1
1
该方法是Point类的一部分,因此必须将其定义为其公共部分的一部分(public)。 相应的实现出现在point.cpp文件中,其实现代码如下:

bool Point::operator==(Point rhs) {
return x == rhs.x && y == rhs.y;
}
1
2
3
1
2
3
与通过接口导出的类的一部分的其他方法一样,operator ==的实现必须通过在方法名称之前添加Point ::前缀来指定它与Point类相关联。所以我们可以这样使用这个方法:

if (pt == origin) ....
1
1
假设pt和origin都是Point类型的变量,当遇到这个表达式时,编译器会从Point类调用==运算符。由于operator ==是一种方法,编译器将变量pt指定为接收方,然后将origin的值复制给参数rhs。在运算符==方法的主体中,对整个x和y的引用是指变量pt中的成员,而rhs.x和rhs.y表达式是指变量origin中的成员。
operator ==方法的代码提供了面向对象编程的重要属性的有用说明。operator ==方法显然可以访问当前对象的x和y字段,因为类中的任何方法都可以访问自己的私有变量。 可能更难理解的是,该变量持有完全不同的对象,operator ==方法也可以访问rhs的私有变量吗?没错,这些引用在C++中是合法的,因为类的private部分中的定义对类是私有的,而不是对象。类的方法的代码可以引用该类的任何对象的实例变量。

基于自由函数的运算符重载

我们容易发现基于方法的运算符重载很容易使得我们混淆,因为编译器对待操作数是不同的。它指定一个作为接收方,另一个作为参数的做法常常让人分不太清楚。恢复对称性的最简单的方法是使用将运算符定义为自由函数的替代方法。 使用此策略,则point.h接口需要包含以下原型:

bool operator==(Point p1, Point p2);
1
1
这个原型声明一个自由函数,因此必须出现在Point类的定义之外。 相应的实现不再包含Point ::前缀,因为运算符不再是类的一部分,其实现看起来像这样:

bool operator==(Point p1, Point p2) {
return p1.x == p2.x && p1.y == p2.y;
}
1
2
3
1
2
3
虽然这个实现更容易去理解,因为它对称地对待参数p1和p2。但是这个代码有一个重大的问题:它实际上并不工作。 如果将这个定义添加到Point类中,你的代码甚至不会编译。问题的关键在于==运算符现在被定义为一个空闲函数,因为它无法访问私有实例变量x和y(private)。
但是C++可以以另一种方式解决访问问题。假设==运算符出现在point.h接口中,它在概念上与Point类相关联,因此在某种意义上应该具有对类的私有变量的访问权限。
为了使此设计工作,Point类必须让C++编译器知道对于特定函数(在这种情况下为==操作符的重载版本)可以查看其私有实例变量。为了使这种访问合法,Point类必须将operator ==函数指定为朋友(friend)。在这种情况下,友谊与社交网络具有同等的特征,比如您的私人信息通常不会与整个社区共享,但你可以允许你的朋友访问。
在C++中,将一个自由函数声明为朋友(国内很多书籍称为友元函数,我个人觉得就是朋友,没必要那么拗口)的语法如下:

friend prototype;
1
1
其中原型(prototype)是完整的函数原型。在这里,例如,你要指定自由函数operator ==是通过写入Point类的一个朋友函数。你可以这样写:

friend bool operator==(Point p1, Point p2);
1
1
当然,C++中不仅仅一个函数可以作为类中的朋友,类与类之间也可以有朋友的关系,语法如下:

friend class name;
1
1
name就是被指定的朋友的类的名字。在C++中,这种友谊的声明不是自动相互的。如果两个类都希望访问另一个类的私有变量,那么每个类都必须将其他类显式声明为一个朋友。(就好比,两个人是朋友的条件是,他把你当朋友,你也把他当朋友,而不是单方面的认为他是你朋友)。
每当你重载一个类的==运算符时,最好为!=运算符提供重载的定义。 毕竟,客户会期望他们能够测试两点是否不同,因为它们可以测试这些点是否相同。 C++不假定 == 和!=返回相反的值。如果你想要这种行为,你必须分别重载这些运算符。但是,您可以在实现operator!=时使用operator ==,因为operator ==是该类的公共成员。因此,最直接的实现就是这样:

bool operator!=(Point p1, Point p2) {
return !(p1 == p2);
}

C++抽象编程·运算符重载与友元函数的相关教程结束。

《C++抽象编程·运算符重载与友元函数.doc》

下载本文的Word格式文档,以方便收藏与打印。