第15章友元、异常和其他
本章内容包括:
- 友元类
- 友元类方法。
- 嵌套类。
- 引发异常、try块和 catch块。
- 异常类。
- 运行阶段类型识别(RTTI)。
- dynami_cast和 typeid
- static_cast、 const_cast和 reinterpret_cast
本章先介绍一些C++语言最初就有的特性,然后介绍C++语言新增的一些特性。前者包括友元类、友元成员函数和嵌套类,它们是在其他类中声明的类;后者包括异常、运行阶段类型识别(RTTI)和改进后的类型转换控制。
C++异常处理提供了处理特殊情况的机制,如果不对其进行处理,将导致程序终止。RTTI是一种确定对象类型的机制。新的类型转换运算符提高了类型转换的安全性。
后3种特性是C++新增的,老式编译器不支持它们。
1. 友元
本书前面的一些示例将友元函数用于类的扩展接口中,类并非只能拥有友元函数,也可以将类作为友元。
在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。
哪些函数、成员函数或类为友元是由类定义的,而不能从外部强加友员。因此,尽管友元被授予从外部访问类的私有部分的权限,但它们并不与面向对象的编程思想相悖;相反,它们提高了公有接口的灵活性。
2. 友元类,一个类是另外一个类的友元
什么时候希望一个类成为另一个类的友元呢?我们来看一个例子。假定需要编写一个模拟电视机和遥控器的简单程序。决定定义ー个Tv类和一个 Remote类,来分别表示电视机和遥控器。很明显,这两个类之间应当存在某种关系,但是什么样的关系呢?遥控器并非电视机,反之亦然,所以公有继承的is-a关系并不适用。遥控器也非电视机的一部分,反之亦然,因此包含或私有继承和保护继承的has-a关系也不适用。
事实上,遥控器可以改变电视机的状态,这表明应将 Romote类作为Tv类的一个友元。首先定义Tv类。可以用一组状态成员(描述电视各个方面的变量)来表示电视机。下面是一些可能的状态:
- 开/关
- 频道设置
- 音量设置
- 有线电视或天线调节模式;
- TV调谐或A/V输入
调节模式指的是,在美国,对于有线接收和UHF广播接收,14频道和14频道以上的频道间隔是不同的。
输入选择包括TV(有线TV或广播TV)和DVD。有些电视机可能提供更多的选择,如多种DVD蓝光输入,但对于这个示例的目的而言,这个清单足够了。
另外,电视机还有一些不是状态变量的参数。例如,可接收频道数随电视机而异,可以包括一个记录这个值的成员。
接下来,必须给类提供一些修改这些设置的方法。当前,很多电视机都将控件藏在面板后面,但大多数电视机还是可以在不使用遥控器的情况下进行换台等工作的,通常只能逐频道换台,而不能随意选台。
同样,通常还有两个按钮,分别用来增加和降低音量。
遥控器的控制能力应与电视机内置的控制功能相同,它的很多方法都可通过使用Tv方法来实现。另外,遥控器通常都提供随意选择频道的功能,即可以直接从2频道换到20频道,并不用逐次切换频道。
另外,很多遥控器都有多种工作模式,如用作电视控制器和DVD遥控器。这些考虑因素表明,定义应类似于程序清单15.1。定义中包括一些被定义为枚举的常数。下面的语句使 Remote成为友元类: 友元类的所有方法都可以访问原始类的私有成员和保护成员。也就是说 Remote可以访问 Tv
类里的私有和保护成员。
friend class Remote;
友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。由于 Remote类提到了Tv类,所以编译器必须了解Tv类后,才能处理 Remote类,为此,最简单的方法是首先定义Tv类。也可以使用前向声明( forward delaration),这将稍后介绍。
下面的代码 Remote 的成员函数直接使用 .
运算符的方式直接访问 Tv类的成员变量。
void set_chan(Tv & t, int c) {t.channel = c;}
程序清单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;
程序清单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 Tv 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.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();
// std::cin.get();
return 0;
}
程序的运行结果:
book@book-desktop:~/meng-yue/c++/friend_abnormal/01$ ./use_tv
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" settings:
TV is On
Volume setting = 5
Channel setting = 28
Mode = antenna
Input = TV
book@book-desktop:~/meng-yue/c++/friend_abnormal/01$
上面的代码,如果不在 Tv类的内部声明 Remote为其友元类,会出现什么效果!
class Tv
{
public:
//friend class Remote; // Remote can access Tv private parts
};
编译看一下效果:编译器报错‘int Tv::channel’ is private
没有访问权限
book@book-desktop:~/meng-yue/c++/friend_abnormal/01$ g++ -o use_tv use_tv.cpp tv.cpp
In file included from use_tv.cpp:3:
tv.h: In member function ‘void Remote::set_chan(Tv&, int)’:
tv.h:29: error: ‘int Tv::channel’ is private
tv.h:45: error: within this context
In file included from tv.cpp:3:
tv.h: In member function ‘void Remote::set_chan(Tv&, int)’:
tv.h:29: error: ‘int Tv::channel’ is private
tv.h:45: error: within this context
book@book-desktop:~/meng-yue/c++/friend_abnormal/01$
3. 友元成员函数, 不用把整个类声明为友元类
从上一个例子中的代码可知,大多数 Remote方法都是用Tv类的公有接口实现的。
这意味着这些方法不是真正需要作为友元。事实上,唯一直接访问Tv成员的 Remote方法是 Remote::set chan()
,因此它是唯一需要作为友元的方法。确实可以选择仅让特定的类成员成为另一个类的友元,而不必让整个类成为友元,但这样做稍微有点麻烦,必须小心排列各种声明和定义的顺序。
1. 友元成员函数的排列问题
下面介绍其中的原因。让 Remote::set chan()()成为Tv类的友元的方法是,在Tv类声明中将其声明为友元:
class Tv
{
friend void Remote::set_chan(Tv & t, int c);
};
然而,要使编译器能够处理这条语句,它必须知道 Remote的定义。否则,它无法知道 Remote是类,而 set_chan是这个类的方法。这意味着应将 Remote的定义放到Tv的定义前面。
Remote的方法提到了Tv对象,而这意味着Tv定义应当位于 Remote定义之前。避开这种循环依赖的方法是,使用前向声 ( forward declaration)。为此,需要在 Remote定义的前面插入下面的语句:
class Tv; // forward declaration
这样,排列次序应如下:
class Tv; //forward declaration
class Remote {};
Class TV {};
能否像下面这样排列呢?
class Remote; // forward declaration
class Tv {};
class Remote {};
答案是不能。原因在于,在编译器在Tv类的声明中看到 Remote的一个方法被声明为Tv类的友元前,应该先看到 Remote类的声明和 set_chan()方法的声明。
还有一个麻烦。程序清单15.1的 Remote声明包含了内联代码,例如:
void onoff(Tv & t) { t.onoff(); }
由于这将调用Tv的一个方法,所以编译器此时必须已经看到了Tv类的声明,这样才能知道Tv有哪些方法,但正如看到的,该声明位于 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 tvfm.h
// tvfm.h -- Tv and Remote classes using a friend member
#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;
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_mode(Tv & t);
void set_input(Tv & t);
void set_chan(Tv & t, int c);
};
class Tv
{
public:
friend void Remote::set_chan(Tv & t, int c);
enum State{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;
private:
int state;
int volume;
int maxchannel;
int channel;
int mode;
int input;
};
// 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
如果在tv.cpp和 use_tv.cp中包含 tvfm.h而不是tv.h,程序的行为与前一个程序相同,区别在于,只有一个Remote方法是Tv类的友元,而在原来的版本中,所有的 Remote方法都是Tv类的友元。图15.1说明了这种区别。
本书前面介绍过,内联函数的链接性是内部的,这意味着函数定义必须在使用函数的文件中。
在这个例子中,内联定义位于头文件中,因此在使用函数的文件中包含头文件可确保将定义放在正确的地方。也可以将定义放在实现文件中,但必须删除关键字 inline,这样函数的链接性将是外部的。
顺便说一句,让整个 Remote类成为友元并不需要前向声明,因为友元语句本身已经指出 Remote是一个类
friend class Remote;
4. 其他友元关系-互为对方的友元
除本章前面讨论的,还有其他友元和类的组合形式,下面简要地介绍其中的一些假设由于技术进步,出现了交互式遥控器。
例如,交互式遥控器让您能够回答电视节目中的问题,如果回答错误,电视将在控制器上产生嗡嗡声。忽略电视使用这种设施安排观众进入节目的可能性,我们只看C++的编程方面。
新的方案将受益于相互的友元,一些 Remote方法能够像前面那样影响Tv对象,而一些Tv方法也能影响 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()是内联的,则应在一个单独的方法定义文件中定义它。
5. 共同的友元
需要使用友元的另一种情况是,函数需要访问两个类的私有数据。从逻辑上看,这样的函数应是每个类的成员函数,但这是不可能的。
它可以是一个类的成员,同时是另一个类的友元,但有时将函数作为两个类的友元更合理。
例如,假定有一个 Probe类和一个 Analyzer类,前者表示某种可编程的测量设备,后者表示某种可编程的分析设备。这两个类都有内部时钟,且希望它们能够同步,则应该包含下述代码行:
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是一种类型。