友元
C++控制对类对象私有部分的访问,公有类方法提供唯一的访问途径。这种限制太严格。C++提供另一种形式的访问权限:友元。
● 友元函数
● 友元类
● 友元成员函数
为何需要友元
在为类重载二元运算符时(带两个参数的运算符)常常需要友元。将Time对象乘以实数就属于这种情况。
乘法运算符与其他两种重载运算符的差别:它使用了两种不同的类型。加法和减法运算符都结合两个Time值,而乘法运算符将一个Time值与一个double值结合在一起。这限制了该运算符的使用方式。左侧的操作数是调用对象。
A = B * 2.75;
将被转换为下面的成员函数调用
A = B.operator*(2.75);
但下面的语句又如何呢?
A = 2.75 * B;
2.75 * B
应与B * 2.75
相同,但第一个表达式不对应于成员函数,因为2.75不是Time类型的对象。记住,左侧的操作数应是调用对象,但2.75不是对象。因此,编译器不能使用成员函数调用来替换该表达式。
解决这个难题的一种方式,只能按B*2.75
这种格式编写,不能写出2.75*B
。
非成员函数
然而,另一种解决方式,非成员函数(成员或非成员函数来重载)。非成员函数不是由对象调用的,它使用的所有值(包括对象)都是显式参数。
A = 2.75 * B; //不能对应成员函数
与下面的原型如下:
A = operator*(2.75, B);
该函数的原型如下:
Time operator*(double m, const Time &t);
对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作符对应于运算符函数第二个参数。而原来的成员函数则按相反的顺序处理操作数,double值乘以Time值。
友元函数
使用非成员函数可以按所需的顺序获得操作数(先是double,然后是Time),但引发了一个新问题:非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问。然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称友元函数。
创建友元
将其原型放在类声明中,并在原型声明前加上关键字friend:
friend Time operator*(double m, const Time &t); //类声明
● 虽然operator*()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用。
● 虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同。
编写函数定义。因为它不是成员函数,所以不要使用Time::限定符。另外,不要在定义中使用关键字friend。
Time operator*(double m, const Time &t)
{
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; //利用 t.operator*(m)
}
将Time对象t作为一个整数使用,让成员函数来处理私有值。
注意:如果要类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。
常用的友元:重载<<运算符
假设trip是一个Time对象。为了显示Time的值,前面使用的是Show()。然而,如果可以像下面更好:
cout << trip;
因为<<是可被重载的C++运算符之一。实际上,它已经被重载很多次了。ostream类对该运算符进行了重载,将其转换为一个输出工具。cout是一个ostream对象,它是智能的,能够识别所有的C++基本类型。
因为对于每种基本类型,ostream类声明中都包含了相应的重载的operator<<()定义。一个定义使用int参数,一个定义使用double参数。
因此,使cout能够识别Time对象,一种方法是将一个新的函数运算符定义添加到ostream类声明中。但修改iostream文件是个危险的主意,这样做会在标准接口上浪费时间。通过Time类声明来让Time类知道如何使用cout。
<<的第一个重载版本
要使Time类知道使用cout,必须使用友元函数。第一个是ostream类对象(cout):
cout << trip;
如果使用一个Time成员函数来重载<<
,Time对象将是一个操作数,就像使用成员函数重载*
运算符那样。
trip << cout;
这样会令人迷惑。但通过使用友元函数。
void operator<<(ostream &os, const Time &t)
{
os << t.hours << " hours, " << t.minutes << " minutes";
}
这样使用下面语句:
cout << trip;
按下面这样的格式打印数据:
4 hours, 23 minutes
注意:新的operator<<()定义使用ostream引用os作为它的第一个参数。通常情况下,os引用cout对象,如表达式cout<<trip所示。但也可以将这个运算符用于其他ostream对象,在这种情况下,os将引用相应的对象。
调用cout << trip 应使用cout对象本身,而不是它的拷贝,因此该函数按引用(而不是按值)来传递该对象。这样,表达式cout << trip 将导致os成为cout的一个别名;而表达式cerr<<trip将导致os成为cerr的一个别名。Time对象可以按值或按引用来传递,因为这两种形式都使函数能够使用对象的值。按引用传递使用的内存和时间都比按值传递少。
<<的第二种重载版本
前面介绍的实现存在一个问题。
cout << trip;
但这种实现不允许像通常那样将重新定义的<<运算符与cout一起使用:
cout << "Trip time: " << trip << " (Tuesday)\n"; //不能这样做
了解cout操作一点知识。
int x = 5;
int y = 8;
cout << x << y;
C++从左至右读取输出语句,意味着它等同于:
(cout << x) << y;
正如iosream中定义的那样,<<运算符要求左边是一个ostream对象。显然,因为cout是ostream对象,所以表达式cout << x满足这种要求。然而,因为表达式cout << x位于<< y的左侧,所以输出语句也要求该表达式是一个ostream类型的对象。因此,ostream类将operator<<()函数实现为返回一个指向ostream对象的引用。因此,表达式(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"; //正确
我们将这条语句分成多步,看看它是如何工作的。
首先,调用ostream中的<<定义,它显示字符串并返回cout对象。
cout << "Trip time: "
因此表达式cout << "Trip time: "将显示字符串,然后被它的返回值cout所替代。原来的语句被简化为下面形式:
cout << trip << " (Tuesday)\n";
接下来,程序使用<<的Time声明显示trip的值,并再次返回cout对象。这将语句简化为:
cout << " (Tuesday)\n";
现在,程序使用ostream中用于字符串的<<定义,来显示最后一个字符串,并结束运行。
operator<<()版本还可用于将输出写入到文件中:
#include <fstream>
...
ofstream fout;
fout.open("savetime.txt");
Time trip(12, 40);
fout << trip;
其中最后一条语句将被转换成:
operator<<(fout, trip);
类继承属性让ostream引用能够指向ostream对象和ofstream对象。
重载<<运算符来显示c_name的对象,可使用一个友元函数:
ostream & operator<<(ostream &os, const c_name &obj)
{
os << ...;
return os;
}
mytime3.h
#ifndef MYTIME3_H
#define MYTIME3_H
#include <iostream>
using namespace std;
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; } //内联函数
friend ostream & operator<<(ostream &os, const Time &t); //友元,重载<<运算符
};
#endif // MYTIME3_H
operator*()为友元函数作为内联函数,因为其代码很短。只有在类声明中的原型中才能使用friend关键字。除非函数定义也是原型,否则不能在函数定义中使用该关键字。
mytime3.cpp
#include "mytime3.h"
#include <iostream>
using namespace std;
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;
}
ostream & operator<<(ostream &os, const Time &t)
{
os << t.hours << " hours, " << t.minutes << " minutes";
return os;
}
main.cpp
#include <iostream>
#include "mytime3.h"
using namespace std;
int main()
{
Time aida(3, 35);
Time tosca(2, 48);
Time temp;
cout << "Aida and Tosca:\n";
cout << aida << "; " << tosca << endl;
temp = aida + tosca;
cout << "Aida + Tosca: " << temp << endl;
cout << "10.0 * Tosca: " << 10.0 * tosca << endl;
return 0;
}