友元类
友元类的使用
友元不仅仅适合于友元函数,还可以将类作为友元,在这种情况下,友元类的所有方法都可以访问原始类的私有方法和保护成员,什么时候去使用友元类呢?
两个类之间不存在包含和所属关系,但是我们某一个类中的方法想要调用另一个类中的私有成员和保护成员,这就需要使用到友元类
我们可以通过一个例子来看看友元类的用法,这是例子是电视机和遥控器的例子,遥控器并不是电视机的一部分,它们也不是包含关系,但是遥控器却可以改变电视机的状态,因此我们可以将遥控器类作为电视机类的一个友元
我们可以先编写一个电视机的类作为原始类,其中包含电视机开关状态,频道,音量以及信号源,然后再编写一个遥控器类作为电视机类的友元类,这样遥控器类的所有方法都可以访问电视机类的私有成员和保护成员,这就是友元类的方法
头文件
#ifndef _TV_h #define _TV_h #include<iostream> using namespace std; //电视机类 class Tv { private: enum{off,on};//开关 int state; enum{minval, maxval = 20};//音量 int volume; enum{channelmin = 1, channelmax = 100};//频道 int channel; enum{tv, dvd}; int input;//信号原 public: Tv(int s = off):state(s),volume(5),channel(2),input(tv){} //切换电视开关模式 void onoff(){ state = (state == on) ? off:on; } //增大音量 bool volup(); //减少音量 bool voldown(); //增加频道 void chanup(); //减少频道 void chandown(); //改变信号 void set_input(){input = (input == tv) ? dvd : tv;} //显示电视机设置 void show()const; //友元类,遥控器类,也是友元类的声明 friend class Remote; }; //遥控器类 class Remote { private: //模式 int mode; public: Remote(int m = Tv::tv):mode(m){} //切换Tv类中的模式 void onoff(Tv &t){t.onoff();} //音量增减 bool volup(Tv &t){return t.volup();} bool voldown(Tv &t){return t.voldown();} //频道切换 void chanup(Tv & t){t.chanup();} void chandown(Tv & t){t.chandown();} //设置频道,通过友元类去访问原始类中的保护数据 void set_channel(Tv &t , int m){t.channel = m;} //设置信号源 void set_input(Tv &t){t.set_input();} }; #endif
函数定义
#include"TV.h" //增大音量 bool Tv::volup() { if(volume < maxval) { volume++; return true; } else return false; } //减少音量 bool Tv::voldown() { if(volume > minval) { volume--; return true; } else return false; } //增加频道 void Tv::chanup() { if(channel < channelmax) channel++; else channel = 1; } //减少频道 void Tv::chandown() { if(channel > channelmin) channel--; else channel = 100; } //显示电视机设置 void Tv::show()const { cout<<"Tv is "<< (state == off ? "off":"on" )<<endl; if(state == on) { cout<<"Volume is "<<volume<<endl; cout<<"Channel is "<<channel<<endl; cout<<"Inpiut is "<<( input == tv ? "tv" : "dvd")<<endl; } }
我们可以编写一个测试程序,看看友元类能否修改我们原始类中的私有成员和保护成员
#include"TV.h" int main() { Tv panda; cout<<"Initial setting for Panda TV:\n"; panda.show(); panda.onoff(); panda.show(); cout<<"-------------------------\n"; Remote rm; rm.set_channel(panda,50); rm.set_input(panda); panda.show(); return 0; }
程序运行结果如下:
根据程序运行的结果可以看出,在我们调用遥控器类的两个成员函数时, rm.set_channel(panda,50); rm.set_input(panda);成功的修改了电视机类的私有成员
前向声明及限定友元
在我们上面的程序中,我们的友元类中只有void set_channel(Tv &t , int m){t.channel = m;}这个成员函数是去访问了原始类的私有成员,而其他的成员函数都是通过访问原始类的公有接口去访问原始类的私有成员,因此我们可以只将遥控器类中的void set_channel函数定义为友元,而其他的不需要,因为其他类都是通过公有接口去访问的私有数据
即我们只需要友元类中的set_channel函数定义为友元函数,而其他的函数不需要,因此,我们可以进行如下的修改
在TV类中
class Tv { .... public: friend void Remote::set_channel(Tv & t, int m); ..... }
注意:
在我们上面的代码中,我们这样进行修改会出现两个问题,即编译器无法识别Remote
而我们上面的代码中的friend class Remote,这里虽然编译器也不认识Remote,但是因为有关键字friend class ,所以编译器会将其看做是一个友元类的声明,所以不会进行报错
我们可以将两个类的定义顺序交换,即将遥控器类放在电视机类定义的上面,但是,这样又会导致编译器无法识别遥控器中Tv,这样就导致了一个循环依赖,解决这个问题的方法就是使用前向声明,即在我们的Remote类之前声明Tv类
class Tv; class Remote { ..... }; class Tv { .... };
这样编译器就能够识别Tv是一个类了,但是我们在的Remote的定义中又使用了Tv类中的常量,Tv类是在Remote类后面定义,虽然编译器知道Tv是一个类,但是不知道Tv类中有什么
这样又会出现错误,因此就可以将Tv类中的常量声明也放入到Remote类中,即如下
但是这样又导致了问题,即我们在Remote类中使用了Tv类中的成员函数,而Tv类并没有进行定义,即没有定义而先进行使用,因此再次对代码进行修改,我们不再在Remote类中将函数实现,而是在Remote中仅仅进行函数的声明,将函数声明放在末尾,因为在头文件中进行定义的函数都是内联函数,因此我们加上关键字inline,这样就得到了最后的结果
#ifndef _TV_h #define _TV_h #include<iostream> using namespace std; //声明Tv类 class Tv; //遥控器类 class Remote { private: //模式 enum{off,on};//开关 enum{minval, maxval = 20};//音量 enum{channelmin = 1, channelmax = 100};//频道 enum{tv, dvd};//信号原 int mode; public: Remote(int m = tv):mode(m){} //切换Tv类中的模式 void onoff(Tv &t); //音量增减 bool volup(Tv &t); bool voldown(Tv &t); //频道切换 void chanup(Tv & t); void chandown(Tv & t); //设置频道,通过友元类去访问原始类中的保护数据 void set_channel(Tv &t , int m); //设置信号源 void set_input(Tv &t); }; //电视机类 class Tv { private: int state; int volume; int channel; int input; enum{off,on};//开关 enum{minval, maxval = 20};//音量 enum{channelmin = 1, channelmax = 100};//频道 enum{tv, dvd};//信号原 public: Tv(int s = off):state(s),volume(5),channel(2),input(tv){} //切换电视开关模式 void onoff(){ state = (state == on) ? off:on; } //增大音量 bool volup(); //减少音量 bool voldown(); //增加频道 void chanup(); //减少频道 void chandown(); //改变信号 void set_input(){input = (input == tv) ? dvd : tv;} //显示电视机设置 void show()const; //友元类,遥控器类 friend void Remote::set_channel(Tv &t, int m); }; //切换Tv类中的模式 inline void Remote::onoff(Tv &t){t.onoff();} //音量增减 inline bool Remote::volup(Tv &t){return t.volup();} inline bool Remote::voldown(Tv &t){return t.voldown();} //频道切换 inline void Remote::chanup(Tv & t){t.chanup();} inline void Remote::chandown(Tv & t){t.chandown();} //设置频道,通过友元类去访问原始类中的保护数据 inline void Remote::set_channel(Tv &t , int m){t.channel = m;} //设置信号源 inline void Remote::set_input(Tv &t){t.set_input();} #endif
改写之后的头文件与之前的头文件相比,改写后的Remote类中只有一个函数是Tv类的友元,而之前的头文件中, Remote类中的所有成员函数都是Tv类的友元
互为友元的关系
假设我们技术的进步,不光能通过遥控器控制电视机,电视机也是反馈信息给我们的遥控器,让它们成为一种交互式的器件,这样我们就可以通过C++的编程,也就是可以通过让类彼此成为对方的友元来实现这个功能,即不光Remote是Tv类的友元,Tv类也是Remote类的友元,对于这样的情况,我们需要使用Remote对象的Tv方法,就需要将Tv类的原型放在Remote类的声明之前,并且将Tv类的定义放在Remote类声明之后,让编译器能够有足够的信息去编译这个方法,即如下所示
class Tv;
{
friend class Remote;
public:
void buzz(Remote & rm);
....
};
class Remote
{
friend class Tv;
public:
void bool volup(Tv & t){t.volup();}
....
};
inline void Tv::buzz(Remote & rm)
{
......
}
由于Remote类的声明在Tv类声明的后面,所以可以在Remote类声明中定义
void bool volup(Tv & t){t.volup();,但是Tv中的buzz方法的定义必须在Remote类的后面
嵌套类
在C++中,我们可以将一个类的声明放在另一个类中,在另一个类中声明的类就被称为嵌套类,它通过提供提供新的类型类作用域来避免名称的混乱,包含类的成员函数可以创建和使用被嵌套类的对象,仅仅当类的声明位于公有部分时,才能在包含类的外面使用嵌套类,并且必须使用作用域解析运算符,对类进行嵌套与对类的包含不同,包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而仅仅是定义了一种类型,该类型仅仅在包含嵌套类声明中的类中有效
我们可以使用一个队列的例子来看看嵌套类的使用方法
// 模板类定义,用于实现队列结构
template <class Item>
class Queue
{
private:
// 设置默认队列大小为10
enum{Q_SIZE = 10};
// 内部类,定义队列节点结构
class Node // 定义节点类
{
public:
// 节点存储的数据
Item item;
// 指向下一个节点的指针
Node *next;
// 构造函数,用传入的参数初始化节点
Node(const Item &t) : item(t), next(NULL){}
};
// 队列头节点
Node *front;
// 队列尾节点
Node *rear;
// 当前队列中的元素数量
int items;
// 队列的最大容量
const int qsize;
public:
// 构造函数,接受队列大小参数,默认大小为Q_SIZE
Queue(int qs = Q_SIZE);
// 析构函数,用于清理队列中的所有节点
~Queue();
// 检查队列是否为空
bool isempty() const;
// 检查队列是否已满
bool isfull() const;
// 返回当前队列中的元素数量
int queuecount() const;
// 将一个元素添加到队列尾部
bool enqueue(const Item &item);
// 从队列头部移除一个元素
bool dequeue(Item &item);
};
在这串代码中,我们使用了嵌套类的方法,使用一个类作为队列的节点,嵌套类的优点在于它们可以封装在外部类的作用域内,同时又能够直接访问外部类的私有成员,在这个例子中,Node
类被用来表示队列中的节点,它与 Queue
类紧密相关,因此适合作为其内部类