2.5 操作符重载
二元函数(binary function)是一种带有两个参数的函数。在设计新类时,类中经常会有用于操作对象的二元函数。有时,我们会很自然地使用类似==和+的符号来描述新类的二元函数,而这些符号也被C++用来描述对其自身的数字和其他数据类型的操作。举例来说,如果希望测试两个点是否相等,编写如下的代码看起来会很自然:
point p1, p2;
if ( p1 == p2 )
cout << "Those points are equal."" << endl;
然而新类不能使用==操作符——除非我们已经定义了一个二元函数来指明==操作符的确切含义。事实上,C++允许为新类定义任何操作符的含义。为一个操作符定义新的含义称为重载(overloading)操作符。下面将展示几种常用的重载示例。
2.5.1 重载二元比较操作符
任何新类都可以重载用于“比较是否相等”的==操作符,其方法是通过定义具有特殊名称的函数。新函数的名称是“operate ==”,如下例所示:
bool operator == (const point& p1, const point& p2)
// Postcondition: The value returned is true if p1 and p2
// are identical; otherwise false is returned.
{
return
(p1.get_x( ) == p2.get_x( ))
&&
(p1.get_y( ) == p2.get_y( ));
}
为了使函数返回true,&&表达式的两部分必须都为true——换句话说,p1的x坐标和y坐标必须与p2的x坐标和y坐标完全相同。
除了特殊的名称operator ==之外,这一函数与任何其他的函数毫无差别。它返回的布尔值能够作为真/假值来使用,例如可以用于下述if语句:
if (p1 == p2)…
在程序中使用重载后的操作符就如同使用任何其他的==:将第一个参数放在==的前面,将第二个参数放在==的后面。
在重载操作符时,操作符的通常用法仍旧是可用的。例如,仍旧可以使用==来测试两个整数或两个双精度数是否相等。事实上,在operator ==的体中,也确实使用了通常的==操作符来比较p1.get_x( )和p2.get_x( ),这能够很好地工作。对于每次使用==,编译器决定比较对象的数据类型,并使用适当的比较函数。
有的时候,一旦重载了一个操作符,就可以使用这个重载后的操作符实现另一个操作符。举例来说,假设为point类定义了operator ==,那么就可以马上重载!=作为“不相等”的操作符:
bool operator != (const point& p1, const point& p2)
// Postcondition: The value returned is true if p1 and p2
// are not identical; otherwise false is returned.
{
return !(p1 ==p2);
}
有必要分析一下表达式!(p1 == p2)。我们使用的操作符==是为point定义的重载后的==;如果两个点相同,那么它返回true,否则返回false。随后,利用通常的非操作符“!”将(p1 == p2)的结果取反。因此,如果(p1 == p2)为true,那么!(p1 == p2)则为false,而函数!=将返回false。反之,如果(p1 == p2)为false,那么!(p1 == p2)则为true,而函数!=将返回true。
除了作为单独的函数存在之外,函数operator ==和operator !=也可以定义为成员函数。在本例中,表达式(p1 == p2)中的p1是实际激活成员函数的对象,而p2则是一个参数。事实上,如果把==实现为成员函数,那么p2将是唯一的参数(因为激活成员函数的对象绝不会出现在参数列表中)。选择成员函数还是选择非成员函数只是一个编程风格的问题,但我们更倾向于使用非成员函数,因为非成员函数将两个参数(p1和p2)置于相同的地位;实际上并没有理由说明是p1激活了函数,而不是p2激活了函数(稍后还将阐明,非成员函数利用称为转换(conversion)的特性为类提供了更好的灵活性)。
图2-16所示的是C++中的6个二元操作符,它们经常被重载为新类的二元比较操作符。

图2-16 经常被重载为比较函数的二元操作符
2.5.2 重载二元算术操作符
除了比较操作符之外,C++中许多其他的二元操作符也都能够被新类重载。例如,操作符+、–、*和 / 通常被认为是算术操作符,而它们都可以被新类重载。举一个更自然一些的例子,物理学家经常使用点作为能够相加的对象,而点的加法则是通过将x坐标和y坐标分别相加而得到的。如果能将前文中的两个点相加,那么将可以编写出如下的程序:
point speed1(5,7);
point speed2(1, 2);
point total;
total = speed1 + speed2;
//将total设置为speed1和speed2的和
cout <<total.get_x( ) << endl;
//输出6
cout << total.get_y( ) << endl;
//输出9
实际上,我们可以通过重载+操作符来为point定义+的含义。重载后的操作符带有两个参数,分别是做加法的两个点。函数返回这两个点的和,如下所示:
point operator + (const point& p1, const point& p2)
// Postcondition: The sum of p1 and p2 is returned.
{
double x_sum, y_sum;
// Compute the x and y of the sum.
x_sum = (p1.get_x( ) + p2.get_x( ));
y_sum = (p1.get_y( ) + p2.get_y( ));
point sum(x_sum, y_sum);
return sum;
}
同二元比较操作符一样,二元算术操作符也能够定义为成员函数,而不只是作为单独存在的函数。成员函数将只具有一个参数,即诸如(p1 + p2)的表达式右边的参数。表达式左边的参数是激活成员函数的对象。通常的编程风格更倾向于将二元操作符实现为非成员函数。
图2-17所示的是C++中的5个二元操作符,它们经常被重载以便执行算术操作。

图2-17 经常被重载为算术函数的二元操作符
2.5.3 重载输出和输入操作符
使用输出操作符 << 和输入操作符 >> 能够对标准C++数据类型进行写入和读取。举例来说,读写一个整数的代码如下所示:
int i;
cin >> i;
//从标准输入中读取i的值。
cout << i;
//将i的值写入标准输出。
毫无疑问,我们也可以对新的point类做相同的处理。
point p;
cin >> p;
//从标准输入中读取p的x坐标和y坐标。
cout << p;
//将p的x坐标和y坐标写入标准输出。
我们可以通过重载操作符<<和>>来为point类提供输入/输出的能力。下面从重载输出操作符开始讲解,其原型如下所示:
ostream& operator <<(ostream& outs, const point& source);
下面逐步讲解这个特殊的函数原型。该函数具有两个参数:outs(是一个ostream)和source(是一个point)。使用这个函数的方法是按照如下的方式列出两个参数:
cout << p;
//第一个参数cout是一个ostream。
//第二个参数p是一个point。
上述cout的数据类型是ostream,意思是“输出流(output stream)”。ostream类是库工具iostream的一部分,该工具也定义了cout(控制台输出设备(console output device)或“标准输出(standard output)”),并且为程序员提供了定义其他输出流的能力(例如连接磁盘文件或打印机的输出流)。函数<<的用意在于:将名称为source的point输出到名称为outs的ostream中。现在可以为函数编写大部分的后置条件:
ostream& operator <<(ostream& outs, const point& source);
// Postcondition: The x and y coordinates of source have been
// written to outs.
参数outs是一个引用参数,这意味着函数能够更改输出流(通过向输出流中写入),而且对其所做的更改将会影响到实参(例如标准输出流cout)。参数source是一个常量引用参数,这意味着函数不会改变正在写入的point。
最后只剩下一处特别的地方:函数的返回类型是ostream&:
ostream& operator <<(ostream& outs, const point& source);
对于大多数情况,这种返回类型意味着函数将返回一个ostream。事实上,函数返回的是它刚刚写入的ostream。这里的符号&(称为引用返回类型(reference return type))具有另外一重含义,我们将在第6章中讲解这个新的含义,而这里只要知道输出和输入操作符都需要一个引用返回类型就已经足够。
在了解上述内容之后,我们现在能够为函数编写完整的后置条件:
ostream& operator <<(ostream& outs, const point& source);
// Postcondition: The x and y coordinates of source have been
// written to outs. The return value is the ostream outs.
函数返回ostream的原因在于C++随后将允许使用输出语句的“串接(chaining)”,如下所示:
cout << "The points are " << p << " and " << q << endl;
该示例一共调用了5个<<函数,其中每一个函数都改变了ostream,并将结果传递到下一个函数调用中。
point的输出操作符的完整实现如图2-18顶部所示,其中大部分工作由以下语句完成:
outs << source.get_x( ) << " " << source.get_y( );
该语句使用普通的<<操作符输出点的坐标,坐标之间用单个空格字符分隔。
函数实现
ostream& operator <<(ostream& outs, const point& source)
// Postcondition: The x and y coordinates of source have been
// written to outs. The return value is the ostream outs.
// Library facilities used: iostream
{
outs << source.get_x( ) << " " << source.get_y( );
//该行语句输出点的坐标,坐标之间用空格字符分隔。
return outs;
}
istream& operator >>(istream& ins, point& target)
// Postcondition: The x and y coordinates of source have been
// read from ins. The return value is the istream ins.
// Library facilities used: iostream
// Friend of: point class
{
ins >> target.x >> target.y;
//该函数必须为友元函数,因为它需要直接访问point类的私有成员。
return ins;
}
图2-18 point的输出和输入操作
point的输入函数的原型与输出函数类似,但它使用的是istream(输入流),而不是ostream,如下所示:
istream& operator >>(istream& ins, point& target)
// Postcondition: The x and y coordinates of target have been
// read from ins. The return value is the istream ins.
输入函数的实现参见图2-18的底端,其中的关键工作由普通的>>操作符完成,操作符读入两个double型数字的语句如下所示:
ins >> target.x >> target.y;
但是这里应当留意,该语句将输入直接发送给point的私有成员变量x和y。只有成员函数才能访问私有成员变量,而这里的输入函数并非point的成员函数。对于这个问题,有两种可能的解决方案:
(1) 编写新的成员函数用于设置point的坐标,然后在输入函数的实现中使用这些新的成员函数。
(2) 由于point类和输入函数都是由相同的程序员来实现和编写,因此可以为输入函数访问point类的私有成员变量准予特殊的许可。
第二种解决方法称为使用友元函数,下面就来讲述这一内容。
2.5.4 友元函数
友元函数(friend function)是一个函数,它并非成员函数,但却仍然可以访问类对象的私有成员。如果要声明友元函数,应当将其函数原型置于类定义中,并在声明的最前面放置关键字friend。
举例来说,为了将point的输入函数声明为一个友元,必须在类定义中插入该友元的原型,如下所示:
class point
{
public:
…
// FRIEND FUNCTION
friend std::istream& operator >>(std::istream& ins, point& target);
//具有新友元的point类。
private:
…
};
一旦友元的原型被放到类定义中,其函数体就可以访问point参数的私有成员,如下所示:
istream& operator >>(istream& ins, point& target)
// Postcondition: The x and y coordinates of source have been
// read from ins. The return value is the istream ins.
// Library facilities used: iostream
// Friend of: point class
{
ins >> target.x >> target.y;
return ins;
}
应当注意,友元函数并非成员函数,因此它不能被类的特定对象激活。友元函数操作的所有信息必须出现在它的参数中。在函数体中简单地书写x或y将是非法的,必须写成target.x和target.y。在本例中,friend operator >>具有一个point参数,而函数可以访问的也就是这个参数的私有成员变量。
这种友元关系可以赋予任何函数,而并非只是操作符函数。但是友元关系应当限制于实现类的程序员所编写的函数——毕竟,这个程序员是唯一真实地了解私有成员的人。通过采用这种方法,关于新类的信息隐藏仍旧被保留。
友元函数
友元函数是一个函数,它并非成员函数,但却仍然需要访问函数参数的私有成员。如果要声明友元函数,应当将友元函数的原型置于类定义中,并在其声明的最前面放置关键字friend。
友元关系应当限制于实现类的程序员所编写的函数。
编程提示 ![]()
何时使用友元函数
在实现一个类时,经常会实现一些附加的函数用于操作类的对象。如果函数需要访问类的私有成员,那么首先应当考虑通过使用成员函数来提供这种访问。然而,如果由于其他的原因,成员函数不方便实现或者不能接受,那么可以为一个函数赋予友元关系,使它可以访问类的私有成员。
2.5.5 Point类—— 内容汇总
在前文中,我们定义了许多新的函数来操作点。总共包括:
● 构造函数
● 两个最初的变更成员函数(shift和rotate90),两个最初的常量成员函数(get_x和get_y)
● 重载后的比较操作符==和!=
● 重载后的算术操作符+,用于将两个点相加
● 重载后的输出和输入操作符
● 2.4节中的函数middle、rotations_needed、rotate_to_upper_right和distance
还可以继续增加更多的point函数,但这将永远也无法完成第2章的讲授。因此到此为止,我们将前文讲述的大部分内容收录进一个新的改进后的point类。新类的头文件newpoint.h如图2-19所示。应当注意的是,头文件需要使用命名空间std中的ostream和istream。但是,using语句绝不能出现在头文件中(参见2.3节),因此这里使用了完整名称std::ostream和std::istream。
类的实现应当位于单独的文件newpoint.cxx中。newpoint.cxx中应当包含什么内容(参见后文的自测习题32)。
当我们提供用于操作类的函数或操作符时,应当遵循如下列表以便获得可靠的信息隐藏。
头文件中:
● 文档说明,其中包括每个函数的前置条件/后置条件协议
● 新类的类定义
● 普通函数(既不是成员函数,也不是友元函数)的原型
实现文件中:
● include指令,用于包含头文件
● 成员函数的实现(不包括内联函数)
● 友元函数和其他非成员函数的实现
头文件
// FILE: newpoint.h (revised from point.h in Figure 2.9 on page 58)
// CLASS PROVIDED: point (an ADT for a point on a two-dimensional plane)
//
// CONSTRUCTOR for the point class:
// point(double initial_x = 0.0, double initial_y = 0.0)
// Postcondition: The point has been set to (initial_x, initial_y).
//
// MODIFICATION MEMBER FUNCTIONS for the point class:
// void shift(double x_amount, double y_amount)
// Postcondition: The point has been moved by x_amount along the x axis
// and by y_amount along the y axis.
//
// void rotate90( )
// Postcondition: The point has been rotated clockwise 90 degrees.
//
// CONSTANT MEMBER FUNCTIONS for the point class:
// double get_x( ) const
// Postcondition: The value returned is the x coordinate of the point.
//
// double get_y( ) const
// Postcondition: The value returned is the y coordinate of the point.
//
// NONMEMBER FUNCTIONS for the point class:
// double distance(const point& p1, const point& p2)
// Postcondition: The value returned is the distance between p1 and p2.
//
// point middle(const point& p1, const point& p2)
// Postcondition: The point returned is halfway between p1 and p2.
//
// point operator +(const point& p1, const point& p2)
// Postcondition: The sum of p1 and p2 is returned.
//
// bool operator ==(const point& p1, const point& p2)
// Postcondition: The return value is true if p1 and p2 are identical.
//
// bool operator !=(const point& p1, const point& p2)
// Postcondition: The return value is true if p1 and p2 are not identical.
//
// ostream& operator <<(ostream& outs, const point& source)
// Postcondition: The x and y coordinates of source have been
// written to outs. The return value is the ostream outs.
//
// istream& operator >>(istream& ins, point& target)
// Postcondition: The x and y coordinates of target have been
// read from ins. The return value is the istream ins.
//
// VALUE SEMANTICS for the point class:
// Assignments and the copy constructor may be used with point objects.
图2-19 新的point类的头文件
#ifndef MAIN_SAVITCH_NEWPOINT_H
#define MAIN_SAVITCH_NEWPOINT_H
#include <iostream> // Provides ostream and istream
namespace main_savitch_2B
//使用新的命名空间,避免与2.4节中的另一个point类发生冲突。
{
class point
{
public:
// CONSTRUCTOR
point(double initial_x = 0.0, double initial_y = 0.0);
// MODIFICATION MEMBER FUNCTIONS
void shift(double x_amount, double y_amount);
void rotate90( );
// CONSTANT MEMBER FUNCTIONS
double get_x( ) const { return x; }
double get_y( ) const { return y; }
// FRIEND FUNCTION
friend std::istream& operator >>(std::istream& ins, point& target);
//友元函数的原型
private:
double x, y; // x and y coordinates of this point
};
// NONMEMBER FUNCTIONS for the point class
double distance(const point& p1, const point& p2);
point middle(const point& p1, const point& p2);
point operator +(const point& p1, const point& p2);
bool operator ==(const point& p1, const point& p2);
bool operator !=(const point& p1, const point& p2);
std::ostream& operator <<(std::ostream & outs, const point& source);
//非成员函数的原型
}
#endif
图2-19 (续)
2.5.6 操作符重载的总结
前文已经提到了多个可以在新类中被重载的C++操作符。实际上,C++中总共有44个这种类型的操作符,而本书今后将只使用本章中讲述的操作符以及另外两个下两章即将介绍的赋值操作符。
程序设计因重载操作符的不同而风格迥异,但我们最终采用的样式应当清晰而一致。本书中操作符重载的原则在图2-20中列出。
|
二元比较操作符 == != <= >= < > |
重载为带有两个参数的非成员函数,返回布尔值 |
参见2.5节中的point示例 |
|
二元算术操作符 + – * / % |
重载为带有两个参数的非成员函数 |
参见2.5节中的point示例 |
|
输入和输出 >> << |
重载为非成员函数,返回istream或者ostream |
参见2.5节中的point示例 |
|
辅助的赋值操作符 += –= 等 |
如果操作符+被重载,那么通常也将+=重载为成员函数,以便使x += y与x = x + y具有相同的效果 |
参见3.1节中的bag示例 |
|
赋值操作符 = |
如果希望x = y不仅仅是将对象y的成员变量复制到对象x中,那么必须把=重载为成员函数 |
参见4.3节中的bag示例 |
图2-20 操作符重载的原则
2.5.7 自测习题
27. 为throttle类重载操作符<。如果第一个throttle的流量小于第二个throttle的流量,函数应当返回true。
28. 为point重载操作符–,并作为二元算术操作符。
29. 为什么友元函数应当由实现类的程序员编写?
30. 如下所示是point类的友元输入函数的实现,错误的地方在哪里?
istream& operator >> (istream& ins, point& target)
// target has x and y data members
// friend of : point class
{
ins >> x >> y;
return ins;
}
31. 为throttle重载输出操作符,输出当前流量的100倍,并在其后紧跟符号%。
32. 实现文件newpoint.cxx中应当包含什么内容?







