首页 > 代码库 > C++学习笔记之友元
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)const34 {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() const42 {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 Remote34 {35 private:36 int mode; //控制是TV或者DVD37 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的私有成员channel46 即使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 else12 return false;13 }14 bool Tv::voldown()15 {16 if (volume > MinVal)17 {18 volume--;19 return true;20 }21 else22 return false;23 }24 void Tv::chanup()25 {26 if (channel < maxchannel)27 channel++;28 else29 channel = 1;30 }31 void Tv::chandown()32 {33 if (channel > 1)34 channel--;35 else36 channel = maxchannel;37 }38 39 void Tv::settings() const40 {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 Tv30 {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
五、一点补充:共同友元
有时为了让一个函数能够同时访问两个类的私有数据,可以让该函数同时成为这两个类的友元。