11.3 友元

友元

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;
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光开朗男孩

你的鼓励是我最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值