当前位置: 代码迷 >> 综合 >> C++ day30 友元(一)友元类, 友元成员函数
  详细解决方案

C++ day30 友元(一)友元类, 友元成员函数

热度:7   发布时间:2024-02-04 20:00:11.0

“友元”就是“朋友”,可能这么翻译显得官方大气一些吧

友元一共有三种:

  • 友元函数
  • 友元类
  • 友元成员函数
    第一种在这里学了;第二种和第三种在本文学习。

注意友元的强大权限:可以从外部访问类的私有部分!

虽然友元的权限有点太大了,都能从外部直接访问类的私有数据了,但是这并没有和OOP的数据隐藏相悖,而是提供了一种扩展类的公有接口的方式,提高了访问类的灵活性

文章目录

  • 友元类:不分你我,没有界限的朋友(==单向==友情)
    • 示例
  • 友元类方法(友元成员函数):分点你我,有点界限的朋友(也是==单向==友情)
    • 引入(很麻烦!要注意好几个顺序!前向声明)
    • 实现(循环依赖,可见性)
    • 示例
  • 比较友元类和友元成员函数
  • 其他友元关系
    • 两个类互为对方的友元类(==双向==友情)
    • 共同的友元(一个普通函数同时是两个类的友元,也需要前向声明)
  • 总结

友元类:不分你我,没有界限的朋友(单向友情)

把其他类作为朋友,
之前只学了友元函数,即把函数作为友元,并且友元函数是类的一种扩展接口,因为外部程序通过友元函数也可以访问类的私有数据成员!

现在再引入友元类,即把另一个类当做当前类的朋友,这样做有什么好处呢?朋友的权限总是比较大的:友元类的所有方法都可以访问原始类的私有成员和保护成员。

为什么需要友元类这种设计呢?那是因为有A类需要访问B类的私有数据的这种现实需求。一个类需要改变另一个类。举个例子:电视机和遥控器,TV类和Remote类,不是is-a关系也不是has-a关系,所以公有继承私有继承保护继承包含等等等等全部靠边站,遥控器需要修改电视机的状态,即Remote类要修改TV类的私有数据成员,这种现实应用的需求就需要使用友元类的设计。把Remote类设置为TV类的友元,则遥控器就可以操作电视机了。

需要注意的是,如果A类是B类的友元类,并不代表B类也是A类的友元类啊!!!这种友情是单向的,不是双向的(后面会简单介绍,双向的友元类关系)。

示例

代码

//tv.h -- TV and Remote classes
#ifndef TV_H_
#define TV_H_class Tv{
public:friend class Remote;//友元类enum {Off, On};enum {MinVal, MaxVal = 20};enum {Antenna, Cable};enum {TV, DVD};Tv(int s = Off, int mc = 125):state(s), volume(5),maxchannel(mc), channel(2),mode(Cable),input(TV){}void onoff(){state = (state==On)?Off:On;}bool ison() const {return state==On;}//const成员函数,不改变调用对象的值bool volup();//调大音量,调大成功则返回truebool voldown();void chanup();//频道加1void chandown();void setMode(){mode = (mode==Antenna)?Cable:Antenna;}void setInput(){input = (input==TV)?DVD:TV;}void settings() const;//显示所有设置private:int state;//on or offint volume;int maxchannel;//最大频道数int channel;//当前频道int mode;//模式,天线(广播)或者有线电视int input;//输入,DVD或者电视};
//友元类,并不是写在Tv类的内部哦
class Remote
{
private:int mode;//控制TV还是DVD
public:Remote(int m = Tv::TV):mode(m){}//使用Tv类的公有数据成员要用类名限定符,因为他们有类作用域//遥控器对电视的控制操作都是通过某个具体的电视类的对象来操作的,即调用电视类的方法,以操作具体某个电视bool volup(Tv & tv){return tv.volup();}//友元类的方法可以和原类的方法同名bool voldown(Tv & tv){return tv.voldown();}void onoff(Tv & tv){tv.onoff();}void chanup(Tv & tv){tv.chanup();}void chandown(Tv & tv){tv.chandown();}void set_chan(Tv & tv, int c){tv.channel = c;}//直接访问和操作私有数据void set_mode(Tv & tv){tv.setMode();}void set_input(Tv & tv){tv.setInput();}
};
#endif // TV_H_
//tv.cpp -- methods for the Tv class
#include <iostream>
#include "tv.h"bool Tv::volup()
{if (volume < MaxVal){++volume;return true;}return false;
}bool Tv::voldown()
{if (volume > MinVal){--volume;return true;}return false;
}void Tv::chanup()
{if (channel < maxchannel)++channel;elsechannel = 1;
}void Tv::chandown()
{if (channel > 1)--channel;elsechannel = maxchannel;
}void Tv::settings() const
{using std::cout;using std::endl;cout << "TV is " << (state==On?"on":"off") << endl;if (state==On){cout << "Volume setting: " << volume << endl;cout << "Channel setting: " << channel << endl;cout << "Mode: " << (mode==Antenna?"Antenna":"Cable") << endl;cout << "Input: " << (input==TV?"TV":"DVD") << endl;}
}
//main.cpp -- 测试程序,一个遥控器可以控制两台电视
#include <iostream>
#include "tv.h"int main()
{using std::cout;Tv s42;//创建一个电视类对象cout << "Initial settings for 42\" TV:\n";s42.settings();s42.onoff();//打开电视s42.chanup();cout << "\nAdjusted setting for 42\" TV :\n";s42.chanup();cout << "\nAdjusted setting for 42\" TV :\n";s42.settings();Remote grey;//遥控器对象grey.set_chan(s42, 10);grey.volup(s42);grey.volup(s42);cout << "\n42\" settings after using remote:\n";s42.settings();Tv s58(Tv::On);//另一台电视s58.setMode();//转为广播模式grey.set_chan(s58, 34);cout << "\n58\" settings:\n";s58.settings();return 0;}

输出

Initial settings for 42" TV:
TV is offAdjusted setting for 42" TV :Adjusted setting for 42" TV :
TV is on
Volume setting: 5
Channel setting: 4
Mode: Cable
Input: TV42" settings after using remote:
TV is on
Volume setting: 7
Channel setting: 10
Mode: Cable
Input: TV58" settings:
TV is on
Volume setting: 5
Channel setting: 34
Mode: Antenna
Input: TV

太久没写cpp代码了,5月到6月一直在学数据结构,只是写点小函数,实现数据结构,很久没用source insight 写类了。有一点手生,忘了一些小细节,比如忘记包含自己写的头文件,忘记自己单独写方法文件时候要用类名限定符限定方法名,甚至忘记一写测试main就先写上return 0,还忘了const成员函数啦。还忘了enum的使用。我发现enum在类中写常量很常见 。才一个月,你说气人不。

我的问题:

  • 写这种一整个类,先写头文件,在里面定义类,可以内联实现的就尽量内联,然后不能内联的长点的函数就开个方法文件,后缀.cpp,包含头文件就好了。测试文件用main函数,也要包含头文件。又开始设计类了,研究一个问题到底写几个类,每个类要什么方法,两个类之间要用什么关系来联系方便操作。比如这里,遥控器类可以操控电视机类对象的私有变量,那就要用友元类或者友元成员函数来实现。毕竟你不能写一个很大的笨拙的类把电视类和遥控器来揉在一起,虽然可以访问私有变量了,但是关系更复杂不好解释了,也不便于后期维护。

  • 我在方法文件中对私有变量也用类名限定了
    实际上,当包含了类的头文件a.h后,在a类的方法文件a.cpp中就不需要用类名限定符限定类a的私有变量了,只需要限定一下方法名。
    在这里插入图片描述

友元类方法(友元成员函数):分点你我,有点界限的朋友(也是单向友情)

友元类的限制太松了,是比较仗义的不分你我的朋友,友元类方法则是更加保守一点的,有点界限的友情。君子之交淡如水嘛,何必是朋友就要享受过多福利呢。所以友元类方法的规则是:只把特定的成员函数指定为另一个类的友元。

A类的成员函数是不是B类的友元,是由A类自己决定和指定的,外部不可强加友情。

引入(很麻烦!要注意好几个顺序!前向声明)

其实这种循环依赖和前向声明的例子我在华为软挑的时候也见到过。当时一个类是图的边,一个类是顶点,就是相互依赖的,所以我就用到了前向声明,不过当时不知道这个概念。

上面的例子中,我们把遥控器类作为电视类的友元类,于是遥控器类的对象就可以访问到电视类的所有私有变量。但是仔细看看遥控器类的方法吧,其实大多数方法都是通过调用电视类的公有接口实现的,而不是一上来就去访问和修改人家的私有数据,是很有礼貌的朋友,毕竟你去朋友家最好只在客厅等公共活动区待着,如果需要什么东西,让朋友去各个房间给你拿,而不是自己进去人家的屋子里乱找,就算人家同意你这样,你也会尽量不那样做的。这里是一样道理,能够调用电视类公有接口解决的问题,我就不去访问人家的私有变量;除非公有接口没有设计我需要的功能,我才不得已去访问和修改人家的私有变量。

这里,电视类没有设计setChan方法,所以遥控器类才去访问channel私有变量的。而且遥控器类的所有方法里,就只有set_chan方法访问了电视类的私有数据,所以电视类根本不需要把遥控器类整个类作为朋友,它为了安全着想,毕竟不能太信任他人,万一遥控器类的各个方法乱修改电视类的私有数据呢?所以电视类可以只把遥控器类的set_chan方法作为自己的友元,这就是友元成员函数了。

这里只是举例子,为了讲解友元类和友元成员函数。其实电视类完全可以自己写一个公有接口setChan,这样遥控器类就可以完全通过公有接口实现遥控了,不需要什么友元类和友元成员函数。

实现(循环依赖,可见性)

友元成员函数的实现很麻烦,因为涉及到两个类的循环依赖。

具体解释过程看这张图。

在这里插入图片描述

  • 因为写友元成员函数的声明时,提到了Remote类和set_chan方法,所以要把Remote类和set_chan()方法的声明放到Tv类前面;但是Remote类的方法的参数有Tv类的对象,所以又必须把Tv类的声明放在Remote类前面。循环依赖了,矛盾了,于是提出解决方案——前向声明。即把其中一个类在最前面简单声明一下,class Tv;或者class Remote
  • 那到底前向声明哪一个类呢?如果把A类的a方法声明为B类的友元成员函数,则B类声明这个友元成员函数时,必须能够看到a方法的声明(不是定义哦),不能只是前向声明A类。所以A类的定义要放在B类的定义的前面。这里,即把Remote类的定义要放在Tv类的定义的前面,那就只能前向声明Tv类了。
  • 由于Tv类的定义在Remote类定义后面,所以Remote类定义时所有要用到Tv类对象的方法都只能写声明,不可以写具体定义。而要把自己的方法写在Tv类定义后面,当然方法名要用类名限定符限定。

。。。。一串矛盾,复杂的解决,主要就是前面的看不到后面的,后面的可以看到前面的,所以必须要看到别人的就要在后面,如果自己在别人前面又想用别人的东西,就只能先声明,然后再去别人后面写具体定义。这里的复杂性说白了,就是一个可见性的问题。要看别人依赖别人就使劲往后走,如果出现循环依赖就前向声明,还不行就在类定义中只提供方法的声明。

示例

//tvfm.h -- using a friend member function
#ifndef _TVFM_H_
#define _TVFM_H_class Tv;//前向声明,告诉编译器Tv是一个类
class Remote
{
public://可以有多个publicenum{TV, DVD};
private:int mode;
public:Remote(int m = TV):mode(m){}//所有要调用Tv类方法的方法只写声明;前向声明使得编译器知道Tv是一个类void set_chan(Tv & tv, int c);void set_mode(Tv & tv);void set_input(Tv & tv);bool volup(Tv & tv);bool voldown(Tv & tv);void chanup(Tv & tv);void chandown(Tv & tv);
};
class Tv
{
public:enum {Off, On};enum {Antenna, Cable};enum {MinVal, MaxVal = 20};
private:int mode;//Antenna or Cableint state;//on or offint input;//TV OR DVDint volume;int channel;int maxchannel;
public:Tv(int s = Off,int m = Cable, int v = 5, int in = Remote::TV, int ch = 1, int maxch = 125):mode(m), state(s),input(in),volume(v),channel(ch),maxchannel(maxch){}friend void Remote::set_chan(Tv & tv, int c);//友元成员函数void onoff(){state=(state==On)?Off:On;}bool ison(){return (state == On)?Off:On;}void chanup();void chandown();bool volup();bool voldown();void setMode(){mode = (mode==Antenna)?Cable:Antenna;}void setInput(){input = (input==Remote::TV) ? Remote::DVD : Remote::TV;}//TV类没有自己定义TV和DVD哦void settings() const;};
//Remote类的方法,由于短小,所以仍设计为内联函数
inline void Remote::set_chan(Tv & tv, int c){if (c>0 && c<tv.maxchannel) tv.channel = c;}
inline void Remote::set_mode(Tv & tv){tv.setMode();}
inline void Remote::set_input(Tv & tv){tv.setInput();}
inline bool Remote::volup(Tv & tv){return tv.volup();}//不需要先看到Tv类的volup()定义!!!只要有声明就够了
inline bool Remote::voldown(Tv & tv){return tv.voldown();}
inline void Remote::chanup(Tv & tv){tv.chanup();}
inline void Remote::chandown(Tv & tv){tv.chandown();}
#endif

tv.cpp只需要把头文件改为"tvfm.h"

//tv.cpp -- methods for the Tv class
#include <iostream>
//#include "tv.h"
#include "tvfm.h"

main.cpp也需要改头文件

//main.cpp -- 测试程序,一个遥控器可以控制两台电视
#include <iostream>
//#include "tv.h"
#include "tvfm.h"
int main()
{using std::cout;Tv s42;//创建一个电视类对象cout << "Initial settings for 42\" TV:\n";s42.settings();s42.onoff();//打开电视s42.chanup();cout << "\nAdjusted setting for 42\" TV :\n";s42.settings();s42.chanup();cout << "\nAdjusted setting for 42\" TV :\n";s42.settings();Remote grey;//遥控器对象grey.set_chan(s42, 10);grey.volup(s42);grey.volup(s42);cout << "\n42\" settings after using remote:\n";s42.settings();Tv s58(Tv::On);//另一台电视,外部使用公有常量也要用类名限定s58.setMode();//转为广播模式grey.set_chan(s58, 34);cout << "\n58\" settings:\n";s58.settings();return 0;}

输出

Initial settings for 42" TV:
TV is offAdjusted setting for 42" TV :
TV is on
Volume setting: 5
Channel setting: 2
Mode: Cable
Input: TVAdjusted setting for 42" TV :
TV is on
Volume setting: 5
Channel setting: 3
Mode: Cable
Input: TV42" settings after using remote:
TV is on
Volume setting: 7
Channel setting: 10
Mode: Cable
Input: TV58" settings:
TV is on
Volume setting: 5
Channel setting: 34
Mode: Antenna
Input: TV

出了四五个小问题,看来真的很菜:

  • 忘记了内联函数还可以用关键字inline声明,用久了直接写,还以为没别的办法了。
  • 而且关于内联函数,这里有一点不容易注意到。由于内联函数具有内部链接,即只在翻译单元(一般是一个文件,包括包含的头文件)内可共享。所以这里必须把内联函数定义在头文件中,这样每次只要包含了头文件,就有了内联函数的定义,就可以访问。如果把内联函数写在方法文件.cpp中,那就只在那个方法文件中可见,就会出错,除非去掉inline关键字,即不做内联函数,具有外部链接。关于内联函数和连接性参照这里和这里。
  • 忘记了,外部程序如果不是通过A类的对象使用类A的公有方法或者变量,需要加类名限定符
    比如main程序中,
    在这里插入图片描述在这里插入图片描述

正确版:

Tv s58(Tv::On);//另一台电视,外部使用公有常量也要用类名限定
  • Tv类的构造函数,我把参数顺序写成了state在第二,所以导致main测试程序中的Tv s58(Tv::On);出错。因为只给了一个参数,那就表示第一个参数,所以如果要写Tv s58(Tv::On);,就必须把构造函数的参数顺序确保state在第一个。

很简答的小问题,只不过一个月没敲这种代码,有一点遗忘。

错误版:

Tv(int m = Cable, int s = Off, int v = 5, int in = Remote::TV, int ch = 1, int maxch = 125):mode(m),state(s),input(in),volume(v),channel(ch),maxchannel(maxch){}

正确版:

Tv(int s = Off,int m = Cable, int v = 5, int in = Remote::TV, int ch = 1, int maxch = 125):mode(m), state(s),input(in),volume(v),channel(ch),maxchannel(maxch){}
  • 对const成员函数不敏感,想不到有的函数要设计为const成员函数,或者声明的时候想到了,写定义又忘记在函数头中写const了。

比较友元类和友元成员函数

友元类
在这里插入图片描述
友元成员函数
在这里插入图片描述

其他友元关系

两个类互为对方的友元类(双向友情)

总算有点良心和人味了哈哈。

比如电视和遥控器需要交互,而不只是遥控器控制电视了,比如遥控器可以输入信息,然后电视接收到信息并作出回应。其实现在的遥控器都是这样的,可以用遥控器的数字键像手机那样输入文字信息,或者是电视上有键盘,用遥控器的上下左右键选择字母组成单词。好像这个例子没有说明电视怎么去影响遥控器吧。。心虚

class Tv
{
friend class Remote;
public:void buzz(Remote & r);//电视对象可以操作遥控器对象//由于前面有friend class Remote;所以编译器知道Remote是一个类,不需要前向声明···
};class Remote
{
friend class Tv;
public:void bool volup(Tv & tv){tv.volup();}//由于Tv类的定义已经在前面了,所以这里可以写Tv类的方法···
};
//由于buzz方法需要使用Remote类对象,所以只能等Remote类定义写完才可以写buzz方法的定义,因为编译器才知道Remote对象的结构
inline void buzz(Remote & r)
{···
}

所以类定义中可以只写方法的声明,然后在类外部写定义,这里又分为在头文件里写内联定义和在单独的方法文件写定义;也可以直接在类内部写内联定义。不过像这里涉及到友元,各种顺序就必须严格把握,也许就必须在头文件中类外部写内联定义了。但是无论如果也不能在单独的方法文件中写内联定义哦。内联定义只可以写在头文件中类外部和类内部两个地方

共同的友元(一个普通函数同时是两个类的友元,也需要前向声明)

这个函数可以不是任何类的成员函数,就是一个普通函数。

比如,一个Probe类表示测量设备,一个Analyzer类表示分析设备,我想要实现两个设备的数据同步,那么可以这么写

class Analyzer;//前向声明,以让编译器在Probe类中看到Analyzer时知道它是个类
class Probe
{friend void sync(Analyzer & a, const Probe & p);//把a同步为p的值,sync a to pfriend void sync(Probe & p, const Analyzer & a);//sync p to a···
};
class Analyzer
{friend void sync(Analyzer & a, const Probe & p);friend void sync(Probe & p, const Analyzer & a);···
};
//在这个头文件中写两个sync函数(重载)的内联定义(也可以在其他方法文件中写,但不能是内联定义)
inline sync(Analyzer & a, const Probe & p){···}
inline sync(Probe & p, const Analyzer & a){···}

总结

  • 友元主要是给类提供了更灵活的一种接口形式。
  • 一个类可以把其他函数,其他类,其他类的成员函数作为友元。但是某些时候需要前向声明,需要特别注意类和方法的声明顺序,以正确的组合友元。