计算时间——运算符重载的一个示例(最终版本)

友元

通常,公有类方法提供唯一的访问途径,但有时候这种限制太严格,以至于不适合特定的编程问题。在这种情况下,C++提供了另外一种形式的访问权限:友元。友元有3种:
 友元函数;
 友元类;
 友元成员函数;

通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。下面介绍友元函数,其他两种将在15章介绍。
在为类重载二元运算符时(带两个参数的运算符)常常需要友元。将Time对象乘以实数就属于这种情况。
A=B * 2.75;
将被转换为下面的成员函数调用;
A=B.operator*(2.75);
但下面的语句又如何呢?
A=2.75 * B ;//cannot correspond to a member function
从概念上说,2.75B应与B2.75相同,但第一个表达式不对应于成员函数,因为2.75不是Time类型的对象。记住,左侧的操作数应是调用对象,但2.75不是对象。因此,编译器不能使用成员函数调用来替换该表达式。
解决这个难题的一种方式是,告知每个人(包括程序员自己),只能按B2.75这种格式编写,不能编写成2.75B这是一种对服务器友好-客户警惕的(server-friendly,client-beware)解决方案,与OOP无关。
**然而,还有另一种解决方式——非成员函数(记住,大多数运算符都可以通过成员或非成员函数来重载)。非成员函数不是由对象调用的,它使用的所有值(包括对象)都是显式参数。**这样,编译器能够将下面的表达式:
A = 2.75 * B; //cannot correspond to a member function
与下面的非成员函数调用匹配;
A = operator * (2.75, B);
该函数的原型如下:
Time operator * (double m, const Time & t);
对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于与运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。而原来的成员函数则按相反的顺序处理操作数,也就是说,double值乘以Time值。
使用非成员函数可以按所需顺序获得操作数,但引发了一个新问题:非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问。然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数。

创建友元

创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字friend:
friend Time operator *( double m, const Time &t ); //goes in class declaration
该原型意味着下面两点:
**

**虽然operator *( )函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
 虽然operator *( )函数不是成员函数,但它与成员函数的访问权限相同。
第二步是编写函数定义。因为它不是成员函数,所以不要使用Time::限定符。另外,不要在定义中使用关键字friend,**定义该如下:

**
Time operator * (double m, const Time &t) //friend not used in definition
{
Time result;
long totalminutes = t.hours * mult * 60 +t.minutes * mult;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}

有了上述声明和定义后,下面的语句;
A = 2.75 * B;
将转换为如下语句,从而调用刚才定义的非成员友元函数;
A = operator* (2.75, B);
总之,类的友元函数是非成员函数,其访问权限与成员函数相同。
实际上,按下面的方式对定义进行修改(交换乘法操作数的顺序),可以将这个友元函数编写为非友元函数:
Time operator
(double m, const Time &t)
{
return t * m; //use t.operator
(m)
}**
原来的版本显式地访问t.minutes和t.hours,所以它必须是友元。
这个版本将Time对象t作为一个整体使用,让成员函数来处理私有值,因此不必是友元。然而,将该版本作为友元也是一个好主意。最重要的是,它将该作为正式类接口的组成部分。其次,如果以后发现需要函数直接访问私有数据,则只要修改函数定义即可,而不必修改类原型。

提示:如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。

常用的友元:重载<<运算符

可以对<<运算符进行重载,使之能与cout一起来显示对象的内容。与前面介绍的示例相比,这种重载要复杂些,因此我们分两步(而不是一步)来完成。

假设trip是一个Time对象。为显示Time的值,前面使用的是Show()。然而,如果可以像下面这样操作将更好:cout<<trip; //make cout recognize Time class?
之所以可以这样做,是因为<<是可被重载的C++运算符之一。
实际上,它已经被重载很多次了。最初,<<运算符时C和C++的位运算符,将值中的位左移。ostream类对该运算符进行了重载,将其转换为一个输出工具。
前面讲过,cout是一个ostream类对象,它是智能的,能够识别所有的C++基本类型。这是因为对于每种基本类型,ostream类声明中都包含了相应的重载的operator<<( )定义。也就是说,一个定义使用int参数,一个定义使用double参数等等。因此,要使cout能够识别Time对象,一种方法是将一个新的函数运算符定义添加到ostream类声明中。但修改iostream文件是个危险的注意,这样做会在标准接口上浪费时间。相反,通过Time类声明来让Time类知道如何使用cout。

1.<<的第一种重载版本

要使Time类知道使用cout,必须使用友元函数。这是什么原因呢?因为下面这样的语句使用两个对象,其中一个是ostream类对象(cout):
cout<<trip;
如果使用一个Time成员函数来重载<<,Time对象将是第一个操作数,就像使用成员函数重载*运算符那样。这意味着必须这样使用<<
trip << cout ; //if operator<<( ) were a Time member function
这样会令人迷惑。但通过使用友元函数,可以像下面这样重载运算符:
void operator<< ( ostream & os, const Time & t)
{
os << t.hours << “ hours, “ << t. minutes << “ minutes” ;
}

这样可以使用下面语句:cout << trip;
按下面这样的格式打印数据:4 hours, 23 minutes

友元还是非友元?

新的Time类声明使operator<<()函数成为Time类的一个友元函数。但该函数不是ostream类的友元。
operator<<( )函数接受一个ostream参数和一个Time参数,因此表面看来他必须同时是两个类的友元。然而,看看函数代码就会发现,尽管该函数访问了Time对象的各个成员,但从始至终都将ostream对象作为一个整体使用。
因为operator<<( )直接访问Time对象的私有成员,所以它必须是Time类的友元。但由于它并不直接访问ostream对象的私有成员,所以并不一定必须是ostream类的友元。这很好,因为这就意味着不必修订ostream的定义。

注意,新的operator<<( )定义使用ostream引用os作为它的第一个参数。通常情况下,os引用cout对象,如表达式cout<<trip所示但也可以将这个运算符用于其他ostream对象,在这种情况下,os将引用相应的对象。

不知道其他ostream对象

另一个ostream对象是cerr,它将输出发送到标准输出流——默认为显示器,但在UNIX, Linux和Windows命令行环境中,可将标准错误流重定向到文件。另外,第6章介绍的ofstream对象可用于将输出写入到文件中。通过继承,ofstream对象可以使用ostream的方法,这样,便可以用operator<<( )定义来将Time的数据写入到文件和屏幕上,为此只需传递一个经过适当初始化的ofstream对象(而不是cout对象)。
调用cout<<trip应使用cout对象本身,而不是它的拷贝,因此该函数按引用(而不是按值)来传递该对象。这样,表达式cout<<trip将导致os成为cout的一个别名;而表达式cerr<<trip将导致我os成为cerr的一个别面。Time对象可以按值或按引用来传递,因为这两种形式都使函数能够使用对象的值。按引用传递使用的内存和时间都比按值传递少。

2.<<的第二种重载版本

前面介绍的实现存在一个问题。像下面这样的语句可以正常工作。
cout << trip;
但这种实现不允许像通常那样重新定义的<<运算符与cout一起使用:
cout<< “Trip time: “ << trip << “ (Tuesday)\n”; //can’t do

要理解这样做不可行的原因以及必须如何做才能使其可行,首先需要了解关于cout操作的一点知识。
请看下面的语句:
int x = 5;
int y = 8;
cout << x << y;
C++从左至右读取输出语句,意味着它等同于:
(cout<<x) << y;
正如iostream中定义的那样,
<<运算符要求左边是一个ostream对象。显然,因为cout是ostream对象,所以表达式cout<<x满足这种要求。
然而,因为表达式cout<<x位于<<y的左侧,所以输出语句也要求该表达式是一个ostream类型的对象。
因此,ostream类将operator<<( )函数实现为返回一个指向ostream对象的引用。具体地说,它返回一个指向调用对象(这里是cout)的引用。因此,表达式(cout<<x)本身就是ostream对象cout,从而可以位于<<运算符左侧。

可以对友元函数采用相同的方法。只要修改operator<<( )函数,让它返回ostream对象的引用即可:

ostream & operator<< (ostream & os, const Time &t)
{
os<< t.hours<<” hours, “ << t.minutes<< “ minutes”;
return os;
}

注意,返回类型是ostream &。这意味着该函数返回ostream对象的引用。因为函数开始执行,程序传递了一个对象引用给它,这样做的最终结果是,函数的返回值就是传递给它的对象。也就是说,下面的语句:
cout << trip;
将被转换为下面的调用:
operator<<(cout, trip);
而该调用返回cout对象。因此,下面的语句可以正常工作:
cout<< “ Trip time: “ << trip <<” (Tuesday)\n”; //can do

我们将这条语句分成多步,来看看它是如何工作的。首先,下面的代码调用ostream中的<<定义,它显示字符串并返回cout对象:
cout<< “Trip time: “
因此表达式cout<<”Trip time:”将显示字符串,然后被它的返回值cout所代替。原来的语句被简化为下面的形式:
cout << trip << “ (Tuesday)\n”;
接下来,程序使用<<的Time声明显示trip值,并再次返回cout对象。这将语句简化为:
cout << “ (Tuesday)\n”;
现在,程序使用ostream中用于字符串的<<定义,来显示最后一个字符串,并结束运行。
有趣的是,这个operator<<( )版本还可用于将输出写入到文件中:
#incldue

ofstream fout;
fout.open(“savetime.txt”);
Time trip(12,40);
fout<<trip;
其中最后一条语句将被转换为这样:
operator<<(fout, trip);
另外,正如第8章指出的,类继承属性让ostream引用能够指向ostream对象和ofstream对象。

提示:一般来说,要重载<<运算符来显示c_name的对象,可使用一个友元函数,其定义如下:

ostream & operator<<(ostream & os , const c_name &obj)
{
os<<…; //display object contents
return os; }

程序清单11.10列出了修改后的类定义,其中包括operator*( )和operator<<( )这两个友元函数。它将第一个友元函数作为内联函数,因为其代码很短。(当定义同时也是原型时,就像这个例子中这样,要使用friend前缀)
警告:只有在类声明中的原型才能使用friend关键字。除非函数定义也是原型,否则不能在函数定义中使用该关键字。

程序清单11.10 mytime3.h

//mytime3.h -- Time class with friends
#ifndef MYTIME0_H_
#define MYTIME0_H_
#include<iostream>
class Time {
private:
	int hours;
	int minutes;
public:
	Time();
	Time(int h, int m = 0);
	void AddMin(int m);
	void AddHr(int h);
	void Reset(int h = 0, int m = 0);
	Time operator+(const Time& t)const;
	Time operator-(const Time& t)const;
	Time operator*(double n)const;
	friend Time operator*(double m, const Time& t) {
		return t * m;
	}//inline definition
	friend std::ostream& operator<<(std::ostream& os, const Time& t);
}
#endif

程序清单11.11列出了修改后的定义。方法使用了Time::限定符,而友元函数不适用该限定符。另外,由于在mytime3.h中包含了iostream并提供using声明std::ostream,因此在mytime3.cpp中包含mytime3.h后,便提供了在实现文件中使用ostream的支持。

程序清单11.11 mytime3.cpp

//mytime3.cpp -- implementing Time methods
#include"mytime3.h"

Time::Time() {
	hours = minutes = 0;
}

Time::Time(int h, int m) {
	hours = h;
	minutes = m;
}

void Time::AddMin(int m) {
	minutes += m;
	hours += minutes / 60;
	minutes %= 60;
}

void Time::AddHr(int h) {
	hours += h;
}

void Time::Reset(int h, int m) {
	hours = h;
	minutes = m;
}

Time Time::operator+(const Time& t)const {
	Time sum;
	sum.minutes = minutes + t.minutes;
	sum.hours = hours + t.hours + sum.minutes / 60;
	sum.minutes %= 60;
	return sum;
}
Time Time::operator-(const Time& t)const
{
	Time diff;
	int tot1, tot2;
	tot1 = t.minutes + 60 * t.hours;
	tot2 = minutes + 60 * hours;
	diff.minutes = (tot2 - tot1) % 60;
	diff.hours = (tot2 - tot1) / 60;
	return diff;
}
Time Time::operator*(double mult)const
{
	Time result;
	long totalminutes = hours * mult * 60 + minutes * mult;
	result.hours = totalminutes / 60;
	result.minutes = totalminutes % 60;
	return result;
}
std::ostream& operator<<(std::ostream& os, const Time& t) {
	os << t.hours << " hours, " << t.minutes << " minutes";
	return os;
}

程序清单11.12是一个示例程序。从技术上说,在usetime3.cpp中不必包含头文件iostream,因为在mytime3.h中已经包含了该文件。然而,作为Time类的用户,您并不知道在类代码文件中已经包含了哪些文件,因此您应负责将您编写的代码所需的头文件包含进来。

程序清单11.12 usetime3.cpp

//usetime3.cpp--using the fourth draft of the Time class 
//compile usetime3.cpp and mytime3.cpp together
#include<iostream>
#include"mytime3.h"
int main()
{
	using std::cout;
	using std::endl;
	Time aida(3, 35);
	Time tosca(2, 48);
	Time temp;
	cout << "Aida and Tosca:\n";
	cout << aida << "; " << tosca << endl;
	temp = aida + tosca;//operator+()
	cout << "Aida + Tosca: " << temp << endl;
	temp = aida * 1.17;	//member operator*()
	cout << "Aida * 1.17: " << temp << endl;
	cout << "10.0 * Tosca: " << 10.0 * tosca << endl;

	return 0;
}

下面是程序清单11.10~程序清单11.12组成的程序的输出:

Aida and Tosca:
3 hours, 35 minutes; 2 hours, 48 minutes
Aida + Tosca: 6 hours, 23 minutes
Aida * 1.17: 4 hours, 11 minutes
10.0 * Tosca: 28 hours, minutes

From 《C++ primer plus》
By Suki

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值