c++学习笔记——友元

初识友元

作用:通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限,即可以像函数的公有成员函数访问该函数的私有部分。

友元的三种形式:

(1)友元函数

(2)友元类

(3)友元成员函数

需要友元函数的原因:

在使用类笔记中Time类中重载了*运算符,其中的乘法运算符将一个Time类和一个double值结合在一起,例如A和B是Time类对象:

A = B * 2.75;

回顾之前的知识点,我们都知道重载后的运算符左侧的操作数是调用对象(这里就是B),也就是说上面的语句将会被转化成下面这样:

A = B.operator*(2.75);

也就是说A = B * 2.75这个表达式是成立的,但是将2.75和B反过来的话,也就是A = 2.75 * B;则是不成立的,因为2.75不是类对象,不能调用类的成员函数,左侧的操作数应该是调用对象才对。

解决方案:

(1)告知使用人只能按照B*2.75这样的方式编写;这是一种服务器友好——客户警惕的解决方案;

(2)非成员函数(在使用类的笔记中的表格上大大多数运算符都可通过成员函数或者非成员函数来重载),非成员函数不是由对象调用,它使用的所有值(包括对象)都是显式参数,这样编译器能够编译A = 2.75 * B;

与之相对应的函数调用是:

A = operator*(2.75, B);

对应的函数原型是:

Time operator*(double m, const Time & t);

对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数(double m),运算符表达式右边的操作数对应于运算符函数的第二个参数(const Time & t)。

这样就可以使用非成员函数按照所需的顺序来获取操作数(先double 后Time),但是非成员函数不能访问类的私有数据,解决方案是友元函数。友元函数时一种特殊的非成员函数,它可以访问类的私有成员。

创建友元

第一步:将原型放在类声明中,并在原型中加上关键字friend,例如:

friend Time operator*(double m, const Time & t);

该原型说明了以下两点:

(1)虽然operator*()函数是在类声明中声明的,但是它不是成员函数,所以不能用成员运算符来调用它;

(2)虽然operator*()函数不是成员函数,但它与成员函数的访问权限一样(可以访问对象的私有属性)

第二步:由于它不是成员函数,所以不要使用使用类名::限定符(这里是Time::)来限定它。不需要在定义中使用关键字friend,定义如下:

程序1:


Time operator*(double m, Time & t)
{
    Time result;
    long totalminutes = hours * mult * 60 + 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;
}

定义中的语句:return t * m;使用了成员运算符函数,可以转化成t.operator*(m)其中t是调用函数的·对象。

原来的版本(程序1中),首先它显式地访问t.minutes和t.hours,所以它必须是友元。

这个版本将Time对象t作为一个整体使用,让成员函数来处理私有值,所以不必是友元。

将这个版本作为友元是1有好处的,首先将它作为正式类接口的组成部分。其次,以后需要函数直接访问私有数据,只需要修改函数定义即可,不必修改类原型。

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

作用:可以通过对<<运算符进行重载,使其可以与cout一起来显示对象的内容。

示例:假设trip是一个Time类对象,前面我们使用show函数来显示Time的值,现在尝试使用友元重载<<进而使用cout<<trip1来显示。

关于<<运算符:最开始的<<运算符是位运算符(位左移),ostream类对其进行了重载,将其重载为一个输出工具,它可以识别每种基本类型(像是int类型或者double类型),在ostream类中包含了相应的重载operator<<()定义,要是的cout可以识别Time对象,有两种解决办法:

第一种:将一个新的函数运算符定义添加到ostream类声明中,但是修改iostream文件是一种危险的行为,会在标准接口上浪费时间。

第二种:在Time类声明来让Time类知道如何使用cout:

(1)<<的第一个重载版本

void operator<<(ostream & os, const Time & t)
{
    os << t.hours << " hours, " << t.minutes << " minutes";
}

经过上述的定义,现在可以像下面这样使用它:

cout << trip;//是类对象

 该表达式导致os成为cout的一个别名,这里调用cout<<trip应使用cout对象本身,而不是它的拷贝,所以该函数按引用而不是按值传递cout。而Time对象可以按值也可以按照引用传递,因为这两种形式都使函数能够使用对象的值,这里是按引用传递,因为按引用传递使用的内存和时间都比按值少。

 这个语句相当于下面的:

operator<<(cout, trip);

 上面的operator<<()函数是Time的一个友元函数,但是不是ostream类的友元。虽然operator<<()函数接受一个ostream参数和一个Time参数。表面上看起来必须同时是这两个类的友元,但是该函数访问了Time对象的各个成员,但是从始至终都将ostream对象作为一个整体使用。因为operator()函数直接访问了Time对象的所有成员,所以它必须是Time的友元函数,但它并不直接访问ostream对象的私有成员,所以并不一定是ostream类的友元,故也不用去修改ostream的定义。

(2)<<的第二个版本

首先对第一个版本的缺陷做出解释:

第一个版本的<<运算符存在一些问题,例如它就不能执行:

cout << "Trip time: " << trip << " (Tuesday)\n";

 不可行的原因是:

cout << x << y;

想上面的这条语句是可以通过编译的,因为<<运算符左边必须是一个ostream对象,因为cout是ostream对象,所以cout<<x是满足的,由于ostream类将operator<<()函数实现为返回一个指向ostream对象的引用,所以表达式(cout << x)本身就是一个ostream对象的cout,从而可以位于<<运算符的左侧。

但是第一种重载方式中,以语句cout << "Trip time: " << trip << " (Tuesday)\n";为例,cout << "Trip time: " << trip执行到这里,返回值不是一个ostream对象,所以不能继续再对后面的内容进行输出。

解决方案:修改第一个版本的友元函数,让它返回ostream对象的引用即可。

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

上面重新定义operator<<()函数,注意返回类型时ostream &,这意味着该函数返回一个ostream对象的引用。因为函数开始执行时,程序传递给一个对象引用给它,这样做的最终结果是函数的返回值就是传递给它的对象。

重载运算符:作为成员函数还是非成员函数

截止现在,我们可以选择使用非成员函数或者成员函数实现运算符重载。一般来说,非成员函数就是友元函数,这样它可以直接访问类的私有数据。

例如:在Time类中重载+运算符,有成员函数和非成员函数两个版本:

Time operator+(const Time & t) const;
friend Time operator+(const Time & t1, const Time & t2);

第一个就是成员函数,一个操作数是通过this指针隐式地传递,另一个操作数作为函数参数显式地传递;第二个就是友元版本(非成员函数),两个操作数都作为参数来传递。

所以,非成员函数版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是隐式地传递的调用对象。

T1 = T2 + T3;

这两个原型都与表达式T2+T3匹配,其中T2+T3都是Time类型对象,也就说它会被转换成下面这样:

T1 = T2.operator+(T3);
T1 = operator+(T2, T3);

在定义运算符时,必须选择其中一种格式,不能同时选择两种,也就是说这两种格式都于同一个表达式匹配,同时定义这两种格式被视为二义性错误,导致编译错误。对于某些运算符而言,成员运算符时唯一选择,(前面的笔记表格中有总结),一般情况下,使用非成员函数较好。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值