C++学习笔记之友元

一、引言

C++控制对类对象私有部分(private)的访问,通常只能通过公有的(public)类方法去访问。但是有时候这种限制太严格,不适合特定的问题,于是C++提供了另外一种形式的访问权限:友元。友元有三种:

  • 友元函数
  • 友元类
  • 友元成员函数

二、友元函数

通过让函数称为友元函数,可以赋予该函数与类的成员函数相同的访问权限。为何需要友元呢?在为类重载二元运算符时常常需要友元,关于运算符的重载可以参考我的博文:

C++学习笔记之运算符重载

下面举例说明:

 1 //mytime0
 2 #ifndef MYTIME0_H
 3 #define MYTIME0_H
 4 
 5 class Time
 6 {
 7 private:
 8     int hours;
 9     int minutes;
10 public:
11     Time();
12     Time(int h, int m = 0);
13     void addMin(int m);
14     void addHr(int h);
15     void reset(int h = 0, int m = 0);
16     Time operator*(double n) const;
17     void show() const;
18 };
19 
20 #endif

 

 1 #include <iostream>
 2 #include "mytime0.h"
 3 
 4 Time::Time()
 5 {
 6     hours = minutes = 0;
 7 }
 8 
 9 Time::Time(int h, int m)
10 {
11     hours = h;
12     minutes = m;
13 }
14 
15 void Time::addMin(int m)
16 {
17     minutes += m;
18     hours += minutes / 60;
19     minutes %= 60;
20 }
21 
22 void Time::addHr(int h)
23 {
24     hours += h;
25 }
26 
27 void Time::reset(int h, int m)
28 {
29     hours = h;
30     minutes = m;
31 }
32 
33 Time Time::operator*(double mult)const
34 {
35     Time result;
36     long totalminutes = hours * mult * 60 + minutes * mult;
37     result.hours = totalminutes / 60;
38     result.minutes = totalminutes % 60;
39     return result;
40 }
41 void Time::show() const
42 {
43     std::cout << hours << " hours, "    << minutes << " minutes";
44 }

 

上述代码建立了一个Time类并重载了这个类的*运算符,将一个Time值与一个double值结合在一起。但是,这将会产生一个问题,重载函数使得*运算符左侧的操作数是调用它的对象,即,下面的语句:

A =  B * 1.75;(这里A,B均为Time类对象)

将被转化为:A = B.operator*(1.75);

但是,下面的语句会怎么转化呢?

A = 1.75 * B;

从概念上说,B * 1.75应该与1.75 * B相同,但是因为1.75不是Time类的对象,所以1.75无法调用被重载的*的成员函数operator*(),所以编译器这时候就不知道如何处理这个表达式,将会报错。

如果要求使用Time的*运算符时,只能按照B * 1.75这种格式书写,当然可以解决问题,但是显然不友好。

另一种解决方式--非成员函数。假如我们定义了一个非成员函数重载*:

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

 

则编译器既可以将 A = 1.75 * B与下面的非成员函数A = operator*(1.75, B);

但是如何实现这个函数呢?因为非成员函数不能直接访问类的私有数据,至少常规非成员函数不可以。但是,完事皆有例外,有一类特殊的非成员函数可以访问类的私有成员,即友元函数。

 

创建友元函数

  • 将原型前面加上关键字friend,然后放在类声明中:

 

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

该原型有如下两点特性:虽然operator*()函数是在类中声明的,但是它不是成员函数,因此不能用成员运算符来调用;虽然operator*()不是成员函数,但它与成员函数的访问权限相同。

  • 编写函数定义

因为它不是成员函数,故不能用Time::限定符,特别注意,不要在定义中使用关键字friend,如下:

1 Time operator*(double m, const Time & t) //不要使用friend关键字
2 {
3     Time result;
4     long totalminutes = t.hours * m * 60 + t.minutes * m;
5     result.hours = totalminutes / 60;
6     result.minutes = totalminutes % 60;
7     return result;
8 }

 

然后,编译器就会调用刚才定义的非成员函数将A = 1.75 * B转换为A = operator*(1.75, B)。本人感觉就是重载了operator*()函数,当然是不是如此有待讨论。

总之,记住,类的友元函数是非成员函数,但其访问权限与成员函数相同。

三、友元类

一个类可以将其他类作为友元,这样,友元类的所有方法都可以访问原始类的私有成员(private)和保护成员(protected),也可以根据需要,只将特定的成员函数指定为另一个类的友元,哪些函数,成员函数或类为友元是由类自己定义的,不能外部强加,就好像你愿意把谁当做是你的朋友,是你自己在心里决定的,别人无法强制。

举例说明,假定需要编写一个模拟电视机和遥控器的简单程序。定义一个Tv类和一个Remote,分别表示电视机和遥控器,遥控器可以改变电视机的状态,应将Remote类作为Tv类的一个友元。

 1 /*Tv and Remote classes*/
 2 #ifndef TV_H_
 3 #define TV_H_
 4 class Tv
 5 {
 6 public:
 7     friend class Remote; //声明谁是自己的“好基友”(友元)
 8     enum {Off, On}; //
 9     enum {MinVal, MaxVal};
10     enum {Antenna, Cable};
11     enum {TV, DVD};
12 
13     Tv(int s = Off, int mc =125) : state(s), volume(5), 
14         maxchannel(mc), channel(2), mode(Cable), input(TV) {};
15     void onoff(){state = (state == On)? Off : On;}
16     bool ison() const {return state == On;}
17     bool volup();
18     bool voldown();
19     void chanup();
20     void chandown();
21     void set_mode() {mode = (mode == Antenna) ? Antenna : Cable;}
22     void set_input() {input = (input = TV) ? DVD : TV;}
23     void settings() const; //显示所有设置
24 private:
25     int state;      //开或者关
26     int volume;     //音量
27     int maxchannel; //最多频道数
28     int channel;    //当前频道号
29     int mode;        //Antenna或者Cable模式
30     int input;      //TV或者DVD输入
31 };
32 
33 class Remote
34 {
35 private:
36     int mode;      //控制是TV或者DVD
37 public:
38     Remote(int m = Tv::TV) : mode(m) {}
39     bool volup(Tv & t) {return t.volup();}
40     bool voldown(Tv & t) {return t.voldown();}
41     void onoff(Tv & t) {t.onoff();}
42     void chanup(Tv & t) {t.chanup();}
43     void chandown(Tv & t) {t.chandown();}
44 
45     /*此处,友元类成员函数set_chan()访问了原始类Tv的私有成员channel
46     即使t是Tv的对象,要知道一个类是不允许对象直接访问私有成员的,此处
47     之所以可以,就是因为Remote是Tv“好基友”(友元)的缘故*/
48     void set_chan(Tv & t, int c) {t.channel = c;}
49 
50     void set_mode(Tv & t) {t.set_mode();}
51     void set_input(Tv & t) {t.set_input();}
52 };
53 #endif

 

 

 1 #include <iostream>
 2 #include "tv.h"
 3 
 4 bool Tv::volup()
 5 {
 6     if (volume < MaxVal)
 7     {
 8         volume++;
 9         return true;
10     }
11     else
12         return false;
13 }
14 bool Tv::voldown()
15 {
16     if (volume > MinVal)
17     {
18         volume--;
19         return true;
20     }
21     else
22         return false;
23 }
24 void Tv::chanup()
25 {
26     if (channel < maxchannel)
27         channel++;
28     else
29         channel = 1;
30 }
31 void Tv::chandown()
32 {
33     if (channel > 1)
34         channel--;
35     else
36         channel = maxchannel;
37 }
38 
39 void Tv::settings() const
40 {
41     using std::cout;
42     using std::endl;
43     cout << "TV is " << (state == Off ? "Off" : "On") << endl;
44     if (state == On)
45     {
46         cout << "Volume setting = " << volume << endl;
47         cout << "Channel setting = " << channel << endl;
48         cout << "Mode = " << 
49             (mode == Antenna? "antenna" : "cable") << endl;
50         cout << "Input = " 
51              << (input == TV? "TV" : "DVD") << endl;
52     }
53 }

 

 1 /*usetv*/
 2 #include <iostream>
 3 #include "tv.h"
 4 
 5 int main()
 6 {
 7     using std::cout;
 8     Tv s42;
 9     cout << "Initial settings for 42\" TV: \n";
10     s42.settings();
11     s42.onoff();
12     s42.chanup();
13     cout << "\n Adjusted settings for 42\" TV: \n";
14     s42.chanup();
15     cout << "\n Adjusted settings for 42\" TV: \n";
16     s42.settings();
17 
18     Remote grey;
19 
20     grey.set_chan(s42, 10);
21     grey.volup(s42);
22     grey.volup(s42);
23     cout << "\n42\" settings after using remote:\n";
24     s42.settings();
25 
26     Tv s58(Tv::On);//这反应了一个遥控器可以控制多台电视
27     s58.set_mode();
28     grey.set_chan(s58, 28);
29     cout << "\n58\‘ settings:\n";
30     s58.settings();
31     return 0;
32 }

 

运行结果:

四、友元成员函数

对于上面的例子,大多数Remote方法都是用Tv的共有接口实现的,意味着这些用Tv的共有接口实现的方法不需要作为友元,唯一直接访问Tv成员的Remote方法是Remote::set_chan(),因此它是唯一需要作为友元的方法,所以可以仅让特定的类成员成为另一个类的友元,而不必将整个类成为友元。

让Remote::set_chan()成为Tv友元的方法是:

class Tv
{
     friend void Remote::set_chan(Tv & t, int c);
...   
};

 

要处理上述语句,编译器必须知道Remote的定义,所以Remote的定义应该放在Tv定义前面,问题是Remote::set_chan(Tv & t, int c)使用了Tv类的对象,故而Tv的定义应该放在Remote定义前面,这就产生了矛盾。

为了解决这个矛盾,需要使用一种叫做前向声明(forward declaration),就是在Remote定义之前插入如下语句:

class Tv;

即排列次序如下:

1 class Tv;//前向声明
2 class Remote {...};
3 class Tv {...};

 

能否这样:

1 class Remote ;//前向声明
2 class Tv {...};
3 class Remote {...};

 

这样做是不可以的,因为编译器在Tv类的声明中看到Remote的一个方法被声明为Tv类的友元之前,应该先看到Remote类的声明和set_chan()方法的声明,如果像上面这样,虽然看到了Remote类的声明,但是看不到set_chan()方法的声明。

还有一个问题,比如在Remote声明中包含如下内联函数:

void onoff(Tv & t) {t.onoff();}

也就是说,在声明这个方法的时候,就给出了它的定义,即调用了Tv的一个方法,而Tv的声明(不是前向声明哦)是放在Remote声明后面的,显然会有问题。所以解决方法是,在Remote声明中只包含方法的声明,而不去定义,将实际的定义放在Tv类之后,即

void onoff(Tv & t) ;

编译器在检查该原型时,需要知道Tv是一个类,而前向声明提供了这种信息,当编译器到达真正的方法定义时,它已经读取了Tv类的声明,关于函数内联,后面可以使用inline关键字声明。

给出修改后的头文件:

 1 /*Tv and Remote classes*/
 2 #ifndef TV_H_
 3 #define TV_H_
 4 
 5 class Tv;
 6 
 7 class Remote
 8 {
 9 public:
10     enum State{Off, On};    
11     enum {MinVal, MaxVal = 20};
12     enum {Antenna, Cable};
13     enum {TV, DVD};
14 private:
15     int mode;
16 public:
17     //只声明,不定义
18     Remote(int m =TV) : mode(m) {}
19     bool volup(Tv & t);
20     bool voldown(Tv & t);
21     void onoff(Tv & t);
22     void chanup(Tv & t);
23     void chandown(Tv & t);
24     void set_chan(Tv & t, int c);
25     void set_mode(Tv & t);
26     void set_input(Tv & t);
27 };
28 
29 class Tv
30 {
31 public:
32     friend void Remote::set_chan(Tv & t, int c); //声明谁是自己的“好基友”(友元)
33     enum State{Off, On};    
34     enum {MinVal, MaxVal = 20};
35     enum {Antenna, Cable};
36     enum {TV, DVD};
37 
38     Tv(int s = Off, int mc =125) : state(s), volume(5), 
39         maxchannel(mc), channel(2), mode(Cable), input(TV) {};
40     void onoff(){state = (state == On)? Off : On;}
41     bool ison() const {return state == On;}
42     bool volup();
43     bool voldown();
44     void chanup();
45     void chandown();
46     void set_mode() {mode = (mode == Antenna) ? Antenna : Cable;}
47     void set_input() {input = (input = TV) ? DVD : TV;}
48     void settings() const; //显示所有设置
49 private:
50     int state;      //开或者关
51     int volume;     //音量
52     int maxchannel; //最多频道数
53     int channel;    //当前频道号
54     int mode;        //Antenna或者Cable模式
55     int input;      //TV或者DVD输入
56 };
57 
58 //Remote方法定义为内联函数
59 inline bool Remote::volup(Tv & t) {return t.volup();}
60 inline bool Remote::voldown(Tv & t) {return t.voldown();}
61 inline void Remote::onoff(Tv & t) {t.onoff();}
62 inline void Remote::chanup(Tv & t) {t.chanup();}
63 inline void Remote::chandown(Tv & t) {t.chandown();}
64 inline void Remote::set_chan(Tv & t, int c) {t.channel = c;}
65 inline void Remote::set_mode(Tv & t) {t.set_mode();}
66 inline void Remote::set_input(Tv & t) {t.set_input();}
67 #endif

 

五、一点补充:共同友元

有时为了让一个函数能够同时访问两个类的私有数据,可以让该函数同时成为这两个类的友元。

 

C++学习笔记之友元,古老的榕树,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。