《C++ primer plus》第15章:友元、异常和其他(1)
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
本章先介绍一些 C++ 语言最初就有的特性然后介绍 C++ 语言新增的一些特性。前者包括友元类、友元成员函数和嵌套类它们是在其他类中声明的类;后者包括异常、运行阶段类型识别RTTI和改进后的类型转换控制。C++ 异常处理提供了处理特殊情况的机制如果不对其进行处理将导致程序终止。RTTI 是一种确定对象类型的机制。新的类型转换运算符提高了类型转换的安全性。后 3 种特性是 C++ 新增的老式编译器不支持它们。
友元
本书前面的一些示例将友元函数用于类的扩展接口中类并非只能拥有友元函数也可以将类作为友元。在这种情况下友元类的所有方法都可以访问原始类的私有成员和保护成员。另外也可以做更严格的限制只将特定的成员函数指定为另一个类的友元。哪些函数、成员函数或类为友元是由类定义的而不能从外部强加友情。因此尽管友元被授予从外部访问类的私有部分的权限但它们并不与面向对象的编程思想相悖;相反它们提高了公有接口的灵活性。
友元类
什么时候希望一个类成为另一个类的友元呢?我们来看一个例子。假定需要编写一个模拟电视机和遥控器的简单程序。决定定义一个 Tv 类和一个 Remote 类来分别表示电视机和遥控器。很明显这两个类之间应当存在某种关系但是什么样的关系呢?遥控器并非电视机反之亦然所以公有继承的 is-a 关系并不适用。遥控器也非电视机的一部分反之亦然因此包含或私有继承和保护继承的 has-a 关系也不适用。事实上遥控器可以改变电视机的状态这表明应将 Romote 类作为 Tv 类的一个友元。
首先定义 Tv 类。可以用一组状态成员描述电视各个方面的变量来表示电视机。下面是一些可能的状态:
- 开/关;
- 频道设置;
- 音量设置;
- 有线电视或天线调节模式;
- TV 调谐或 A/V 输入。
调节模式指的是在美国对于优先接收和 UHF 广播接收14 频道和 14频道以上的频道间隔是不同的。输入选择包括TV有线TV或广播TV和 DVD。有些电视机可能提供更多的选择如多种 DVD/蓝光输入但对于这个示例的目的而言这个清单足够了。
另外电视机还有一些不是状态变量的参数。例如可接收频道数随电视机而异可以包括一个记录这个值的成员。
接下来必须给类提供一些修改这些设置的方法。当前很多电视机都将控件藏在面板后面但大多数电视机还是可以在不适用遥控器的情况下进行换台等工作的通常只能逐频道换台而不能随意选台。同样通常还有两个按钮分别用来增加和降低音量。
遥控器的控制能力应与电视机内置的控制功能相同它的很多方法都可通过使用 Tv 方法来实现。另外遥控器通常都提供随意选择频道的功能即可以直接从2频道换到20频道并不用逐次切换频道。另外很多遥控器都有多种工作模式如用作电视控制器和 DVD 遥控器。
这些因素表明定义应类似于下面的程序。定义中包括一些被定义为枚举的常数。下面的语句使 Remote 成为友元类:
friend class Remote;
友元声明可以位于公有、私有或保护部分其所在的位置无关紧要。由于 Remote 类提到了 Tv 类所以编译器必须了解 Tv 类后才能处理 Remote 类为此最简单的方法使首先定义 Tv 类。也可以使用前向声明forward delaration这将稍后介绍。
// 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();
bool 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
在上述程序中大多数类方法都被定义为内联的。除构造函数外所有的 Remote 方法都将一个 Tv 对象引用作为参数这表明遥控器必须针对特定的电视机。下面的程序列出了其余的定义。音量设置函数将音量成员增减一个单位除非声音到达最大或最小。频道选择函数使用循环方式最低的频道设置为1它位于最高的频道设置 maxchannel 之后。
很多方法都使用条件运算符在两种状态之间切换:
void onoff() { state = (state == On) ? Off : On; }
如果两种状态值分别为 true(1) 和 false(0)则可以结合使用将在附录 E 讨论的按位异或和赋值运算符(^=) 来简化上述代码:
void onoff() { state ^= 1; }
事实上在单个无符号 char 变量中可存储多达 8 个双状态设置分别对它们进行切换;但现在已经不用这样做了使用附录 E 中讨论的按位运算符就可以完成。
// tv.cpp -- methods for the Tv class (Remote methods are inline)
#include <iostream>
#include "15.1_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;
}
}
下面的程序是一个简短的程序可以测试一些特性。另外可使用同一个遥控器控制两台不同的电视机。
// use_tv.cpp -- using the Tv and Remote classes
#include<iostream>
#include"15.1_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.voldown(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;
}
这是几个文件组成的程序的输出:
Initial settings for 42" TV:
TV is Off
Adjusted settings for 42" TV:
Adjusted settings for 42" TV:
TV is On
Volume setting = 5
Channel setting = 4
Mode = cable
Input = TV
42" settings after using remote:
TV is On
Volume setting = 5
Channel setting = 10
Mode = cable
Input = TV
58" settings:
TV is On
Volume setting = 5
Channel setting = 28
Mode = antenna
Input = TV
这个练习的主要目的在于表明类友元是一种自然用语用于表示一些关系。如果不使用某些形式的友元关系则必须将 Tv 类的私有部分设置为公有的或者创建一个笨拙的、大型类来包含电视机和遥控器。这种解决方法无法反应这样的事实即同一个遥控器可用于多台电视机。
友元成员函数
从上一个例子中的代码可知大多数 Remote 方法都是用 Tv 类的公有接口实现的。这意味着这些方法不是真正需要作为友元。事实上唯一直接访问 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 的定义放到 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() 方法的声明。
还有一个麻烦。上面的程序中 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 关键字仍然可以使其成为内联方法。下面的程序列出了修订后的头文件。
// tvfm.h -- Tv and Remote classes using 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.cpp 中包含 tvfm.h 而不是 tv.h程序的行为与前一个程序相同区别在于只有一个 Remote() 方法是 Tv 类的友元而在原来的版本中所有的 Remote() 方法都是 Tv 类的友元。
本书前面介绍过内联函数的链接性是内部的这意味着函数定义必须在使用函数的文件中。在这个例子中内联定义位于头文件中因此在使用函数的文件中包含头文件可确保将定义放在正确的地方
。也可以将定义放在实现文件中但必须删除关键字 inline这样函数的链接性将是外部的。
顺便说一句让整个 Remote() 类成为友元并不需要向前声明因为友元语句本身已经指出 Remote 是一个类:
friend class Remote;
其他友元关系
除本章前面讨论的还有其他友元和类的组合形式下面简要地介绍其中的一些。
假设由于技术进步出现了交互式遥控器。例如交互式遥控器让您能够回答电视节目中的问题如果回答错误电视将在控制器上产生嗡嗡声。忽略使用这种设施安排观众进入节目的可能性我们只看 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() 是内联的则应在一个单独的方法中定义它。
共同的友元
需要使用友元的另一种情况是函数需要访问两个类的私有数据。从逻辑上看这样的函数应是每个类的成员函数但这是不可能的。它可以是一个类的成员同时是另一个类的友元但有时将函数作为两个类的友元更合理。例如假定有一个 Probe 类和一个 Analyzer 类前者表示某种可编程的测量设备后者表示某种可编程的分析设备。这两个类都有内部时钟且希望它们能够同步则应包含下述代码行:
class Analyzer; // foward 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 Analyze & a) {
...
}
前向声明使得编译器看到 Probe 类声明中的友元声明时知道 Analyzer 是一种类型。