一、友元类
什么时候希望一个类成为另一个类的友元呢?我们来看一个例子。假定需要编写一个模拟电视机和遥控器的简单程序。决定定义一个Ty 类和个 Remote 类,来分别表示电视机和遥控器。很明显。这两个类之间应当存在某种关系,但是什么样的关系呢?遥控器并非电视机,反之亦然,所以公有继承的 is-a关系并不适用。遥控器也非电视机的一部分,反之亦然,因此包含或私有继承和保护继承的 has-a 关系也不适用。事实上,遥控器可以改变电视机的状态,这表明应将 Romote 类作为Ty 类的一个友元。首先定义 Tv 类。可以用一组状态成员(描述电视各个方面的变量)来表示电视机。下面是一些可能的状态∶
● 开/关;
● 频道设置;
● 音量设置;
● 有线电视或天线调节模式∶
● TV调谐或 A/V输入。
调节模式指的是,在美国,对于有线接收和UHF广播接收,14频道和 14频道以上的频道间隔是不同的。输入选择包括TV(有线TV或广播TV)和 DVD。有些电视机可能提供更多的选择,如多种 DVD/蓝光输入,但对于这个示例的目的而言,这个清单足够了。
另外,电视机还有一些不是状态变量的参数。例如,可接收频道数随电视机而异,可以包括一个记录这个值的成员。
接下来,必须给类提供一些修改这些设置的方法。当前,很多电视机都将控件藏在面板后面,但大多数电视机还是可以在不使用遥控器的情况下进行换台等工作的,通常只能逐频道换台,而不能随意选台。同样,通常还有两个按钮,分别用来增加和降低音量。
遥控器的控制能力应与电视机内置的控制功能相同,它的很多方法都可通过使用 Ty 方法来实现。另外,遥控器通常都提供随意选择频道的功能,即可以直接从2频道换到 20频道,并不用逐次切换频道。另外,很多遥控器都有多种工作模式,如用作电视控制器和 DVD遥控器。
这些考虑因素表明,定义应类似于程序清单 15.1。定义中包括一些被定义为枚举的常数。下面的语句使Remote 成为友元类∶
friend class Remote;
友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。由于Remote类提到了Tv 类,所以编译器必须了解 Tv 类后,才能处理 Remote 类,为此,最简单的方法是首先定义Ty 类。也可以使用前向声明(forward delaration),这将稍后介绍。
程序清单15.1 tv.h:
// tv.h -- TV and Remote classes
#ifndef TV_H_
#define TV_H_
class Tv
{
public:
friend class Remote; // Remote can access Tv private parts
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;}
bool volup ();
bool voldown();
void chanup ();
void chandown ();
void set_mode(){mode=(mode == Antenna)? Cable:Antenna;}
void set_input(){input =(input == TV)? DVD:TV;}
void settings() const;// display all settings
private:
int state;// on or off
int volume;// assumed to be digitized
int maxchannel;// maximum number of channels
int channel;// current channel setting
int mode;// broadcast or cable
int input;// TV or DVD
};
class Remote
{
private:
int mode;// controls TV or DVD
public:
Remote(int m = Tv::TV): mode(m) {}
bool volup(Tv & t){return t.volup();}
bool voldown (Tv & t){ return t.voldown ();}
void onoff (Tv & t){ t.onoff ();}
void chanup (Tv & t){t.chanup ();}
void chandown (Tv & t){t.chandown ();}
void set_chan(Tv & t, int c){t.channel = c;}
void set_mode (Tv & t){t.set_mode (); }
void set_input (Tv & t){t.set_input ();}
};
#endif
在程序清单 15.1 中。大多数类方法都被定义为内联的。除构造函数外。所有的 Romote方法都将一个
Tv 对象引用作为参数,这表明遥控器必须针对特定的电视机。程序清单 15.2 列出了其余的定义。音量设置函数将音量成员增减一个单位,除非声音到达最大或最小。频道选择函数使用循环方式,最低的频道设置为1,它位于最高的频道设置 maxchannel之后。
很多方法都使用条件运算符在两种状态之间切换∶
void onoff(){state =(state == On)? Off: On; }
如果两种状态值分别为 true(1)和 false(0),则可以结合使用将在附录 E 讨论的按位异或和赋值运算符(^=)来简化上述代码∶
void onoff(){state ^= 1;}
事实上,在单个无符号 char变量中可存储多达8个双状态设置,分别对它们进行切换;但现在已经不
用这样做了。
程序清单 15.2 tv.cpp:
// tv.cpp -- methods for the Tv class (Remote methods are inline)
#include <iostream>
#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 < maxchannel)
channel++;
else
channel = 1;
}
void Tv::chandown()
{
if (channel > 1)
channel--;
else
channel = maxchannel;
}
void Tv::settings() const
{
using std::cout;
using std::endl;
cout <<"TV is"<<(state == Off? "Off":"On")<< 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;
}
}
程序清单15.3是一个简短的程序,可以测试一些特性。另外,可使用同一个遥控器控制两台不同的电视机。
程序清单15.3 use_tv.cpp:
//use_tv.cpp -- using the Ty and Remote classes
#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 settings for 42\" TV:\n";
s42.chanup();
cout <<"\nAdjusted settings 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.set_mode();
grey.set_chan(s58, 28);
cout << "\n58\" settings:\n";
s58.settings();
return 0;
}
下面是程序清单15.1~程序清单15.3组成的程序的输出∶
Initial settings for 42" TV:
TV is Off
Adjusted settings for 42" TV:
TV is On
Volume setting = 5
Channel setting = 3
Mode = cable
Input = TV
42" settings after using remote:
TV is On
Volume setting = 7
Channel setting = 10
Mode = cable
Input = TV
58" setings:
TV is On
Volume setting = 5
Channel setting = 28
Mode = antenna
Input = TV
这个练习的主要目的在于表明,类友元是一种自然用语,用于表示一些关系。如果不使用某些形式的友元关系。则必须将 Ty 类的私有部分设置为公有的。或者创建一个笨拙的、大型类来句含申视机和遥控器。这种解决方法无法反应这样的事实,即同一个遥控器可用于多台电视机。
二、友元成员函数
从上一个例子中的代码可知,大多数Remote 方法都是用Ty 类的公有接口实现的。这意味着这些方法不是真正需要作为友元。事实上,唯一直接访问 Tv 成员的 Remote 方法是 Remote::set_chan(),因此它是唯一需要作为友元的方法。确实可以选择仅让特定的类成员成为另一个类的友元,而不必让整个类成为友元,但这样做稍微有点麻烦,必须小心排列各种声明和定义的顺序。下面介绍其中的原因。
让 Remote::set_chan()成为Tv 类的友元的方法是,在 Tv类声明中将其声明为友元∶
class Tv
{
friend void Remote::set_chan (Tv & t,int c);
...
}
然而,要使编译器能够处理这条语句,它必须知道Remote 的定义。否则,它无法知道Remote 是一个
类,而 set_chan 是这个类的方法。这意味着应将 Remote 的定义放到 Ty 的定义前面。Remote 的方法提到了Tv对象,而这意味着Ty定义应当位于Remote定义之前。避开这种循环依赖的方法是,使用前向声明(forward declaration)。为此,需要在 Remote 定义的前面插入下面的语句∶
class Tv; // forward declaration
这样,排列次序应如下∶
class Ty; // forward declaration
class Remote { ...};
class Tv { ...)
能否像下面这样排列呢?
class Remote; // forward declaration
class Tv { ... };
class Remote { ... };
答案是不能。原因在于,在编译器在 Tv 类的声明中看到 Remote 的一个方法被声明为Ty 类的友元之
前,应该先看到 Remote 类的声明和 set_chan()方法的声明。
还有一个麻烦。程序清单 15.1的 Remote 声明包含了内联代码,例如∶
void onoff ('Tv & t){ t.onoff();}
由于这将调用Tv 的一个方法,所以编译器此时必须已经看到了Tv 类的声明,这样才能知道Ty 有哪
些方法,但正如看到的,该声明位于Remote 声明的后面。这种问题的解决方法是,使Remote 声明中只包含方法声明,并将实际的定义放在 Tv 类之后。这样,排列顺序将如下∶
class Tv; / / forward declaration
class Remote{ ...); / /Tv-using methods as prototypes only
class Tv { ...};// put Remote method definitions here
Remote方法的原型与下面类似∶
void onoff (Tv & t);
检查该原型时,所有的编译器都需要知道 Tv 是一个类,而前向声明提供了这样的信息。当编译器到
达真正的方法定义时,它已经读取了 Tv 类的声明,并拥有了编译这些方法所需的信息。通过在方法定义中使用 inline 关键字,仍然可以使其成为内联方法。程序清单15.4列出了修订后的头文件。
程序清单15.4 tvim.h:
// tv.h -- TV and Remote classes
#ifndef TVFM_H_
#define TVFM_H_
class Tv; // forward declaration
class Remote
{
public:
enum State{Off, On};
enum {MinVal, MaxVal = 20};
enum {Antenna,Cable};
enum {TV, DVD};
private:
int mode;// controls TV or DVD
public:
Remote(int m = TV): mode(m) {}
bool volup(Tv & t); // prototype only
bool voldown (Tv & t);
void onoff (Tv & t);
void chanup (Tv & t);
void chandown (Tv & t);
void set_chan(Tv & t, int c);
void set_mode (Tv & t);
void set_input (Tv & t);
};
class Tv
{
public:
friend void Remote::set_chan(Tv & t,int c);
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;}
bool volup ();
bool voldown();
void chanup ();
void chandown ();
void set_mode(){mode=(mode == Antenna)? Cable:Antenna;}
void set_input(){input =(input == TV)? DVD:TV;}
void settings() const;// display all settings
private:
int state;// on or off
int volume;// assumed to be digitized
int maxchannel;// maximum number of channels
int channel;// current channel setting
int mode;// broadcast or cable
int input;// TV or DVD
};
// Remote methods as inline functions
inline bool Remote::volup(Tv & t){ return t.volup();}
inline bool Remote::voldown(Tv & t){ return t.voldown();}
inline void Remote::onoff(Tv & t){t.onoff();}
inline void Remote::chanup(Tv & t){t.chanup();}
inline void Remote::chandown(Tv & t){t.chandown();}
inline void Remote::set_mode(Tv & t){t.set_mode();}
inline void Remote::set_input(Tv & t){t.set_input();}
inline void Remote::set_chan(Tv & t,int c){t.channel = c;}
#endif
三、其他友元关系
除本章前面讨论的,还有其他友元和类的组合形式,下面简要地介绍其中的一些。
假设由于技术进步,出现了交互式遥控器。例如,交互式遥控器让您能够回答电视节目中的问题,如果回答错误,电视将在控制器上产生嗡嗡声。忽略电视使用这种设施安排观众进入节目的可能性,我们只看C++的编程方面。新的方案将受益于相互的友情,一些 Remote方法能够像前面那样影响 Ty 对象,而一些Ty方法也能影响 Remote 对象。这可以通过让类彼此成为对方的友元来实现,即除了 Remote 是Tv的友元外,Tv还是Remote 的友元。需要记住的一点是,对于使用Remote 对象的Tv方法,其原型可在 Remote类声明之前声明,但必须在 Remote类声明之后定义,以便编译器有足够的信息来编译该方法。这种方案与下面类似:
class Tv
{
friend class Remote;
public:
void buzz (Remote & r);
...
};
class Remote
{
friend class Tv;
public:
void Bool volup (Tv & t) { t.volup();}
};
inline void Tv::buzz(Remote & r)
{
...
}
由于Remote 的声明位于Tv 声明的后面,所以可以在类声明中定义Remote::volup(),但Tv::buzz()方法必须在 Tv声明的外部定义,使其位于Remote 声明的后面。如果不希望 buzz()是内联的,则应在一个单独的方法定义文件中定义它。
四、共同的友元
需要使用友元的另一种情况是,函数需要访问两个类的私有数据。从逻辑上看,这样的函数应是每个
类的成员函数,但这是不可能的。它可以是一个类的成员,同时是另一个类的友元,但有时将函数作为两个类的友元更合理。例如,假定有一个 Probe类和一个Analvzer 类,前者表示某种可编程的测量设备,后者表示某种可编程的分析设备。这两个类都有内部时钟,且希望它们能够同步,则应该包含下述代码行:
class Analyzer;// forward declaration
class Probe
{
friend void sync(Analyzer & a, const Probe & p);// sync a to p
friend void sync(Probe & p,const Analyzer & a); // sync p to a
...
};
class Analyzer
{
friend void sync(Analyzer & a,const Probe & p);// sync a to p
friend void sync(Probe & p,const Analyzer & a); // sync p to a
};
// define the friend functions
inline void sync(Analyzer & a,const Probe & p)
{
...
}
inline void sync(Probe & p,const Analyzer & a)
{
...
}
前向声明使编译器看到Probe类声明中的友元声明时,知道 Analyzer 是一种类型。