第十五章 友元、异常和其他
本章内容包括:
- 友元类
- 友元方法
- 嵌套类
- 引发异常、try块和catch块。
- 异常类。
- 运行阶段类型识别(RTTI)。
- dynamic_cast 和 typeid。
- static_cast、const_cast 和 reiterpret_cast。
友元:
前面将友元函数用于类的扩展接口中,类并非只拥有友元函数,也可以将类作为友元。这种情况下友元类的所有方法都可以访问原始类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。哪些函数、成员函数或类为友元是由类定义的,而不能从外部强加友情。因此,尽管友元被授予从外部访问类的私有部分的权限,但它们并不与面向对象的编程思想相悖;相反,它们提高了公有接口的灵活性。
友元类:
电视机与遥控机的关系可以描述为友元。
tv.h
#ifndef D1_TV_H
#define D1_TV_H
class Tv {
private:
int state; // on or off
int volume;
int maxchannel;
int channel;
int mode;
int input;
public:
friend class Remote;
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==Cable)?Antenna:Cable;}
void set_input() {input = (input == TV)?DVD:TV;}
void settings() const ; // display all settings
};
class Remote
{
private:
int mode;
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 //D1_TV_H
tv.cpp
#include "Tv.h"
#include <iostream>
bool Tv::volup() {
if (volume < MaxVal)
{
volume++;
return true;
}
return false;
}
bool Tv::voldown() {
if (volume > MinVal)
{
volume--;
return true;
}
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 == On?"On":"Off") << 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
#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 << "\nAdjustedd 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;
}
结果
Initial settings for 42"TV:
TV is Off
Adjustedd 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
Process finished with exit code 0
例子表明,类友元是一种自然用语,用于表达一些关系。如果不使用某些形式的友元关系,则必须将Tv类的私有部分设置为公有的,或建立一个大型类来包含电视机和遥控器。这种解决方法无法反应这样的事实,一个遥控器可控制多台电视机。
友元成员函数:
上一个例子中,大多数Remote方法都是用Tv类的公有接口来实现的。这意味着这些方法不是真正需要作为友元。事实上唯一直接访问Tv成员的Remote方法是Remote::set_chan(),因此它是唯一需要作为友元的方法。
让Remote::set_chan()成为Tv类的友元方法是,在Tv类中将其声明为友元
tvfm.h
#ifndef D1_TV_H
#define D1_TV_H
// 让Remote 类知道 Tv类
class Tv;
class Remote
{
private:
int mode;
public:
enum {Off, On};
enum {MinVal,MaxVal=20};
enum {Antenna,Cable};
enum {TV,DVD};
Remote(int m = TV) : mode(m){}
bool volup(Tv &t);
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 {
private:
int state; // on or off
int volume;
int maxchannel;
int channel;
int mode;
int input;
public:
// 要让编译器处理这条语句,要先让编译器看到Remote的定义
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==Cable)?Antenna:Cable;}
void set_input() {input = (input == TV)?DVD:TV;}
void settings() const ; // display all settings
};
// 由于调用tv方法前,必须知道Tv方法有哪些函数,因此放到Tv下面
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_chan(Tv &t, int c) {t.channel = c;}
inline void Remote::set_mode(Tv & t){t.set_mode();}
inline void Remote::set_input(Tv & t){t.set_input();}
#endif //D1_TV_H
其他友元关系:
假如电视机也能让遥控器发出声音,也就是互为友元类:
class Tv{
public:
friend class Remote;
void buzz(Remote & r);
......
}
class Remote{
private:
friend class Tv;
void buzz();
......
}
inline void Tv::buzz(Remote & r){
r.buzz();
}
共同的友元:
需要使用友元的另一种情况,函数需要访问两个类的私有数据。这时可以将函数作为两个类的友元。
class Analyzer;
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
......
}
inline void sync(Analyzer & a, const Probe & p){......}
inline void sync(Probe & p, const Analyzer & a){......}
嵌套类:
在另一个类中声明的类被称为嵌套类,它通过提供新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公有部分,才能在类的外面使用嵌套类
Queue.h
template <class Item>
class Queue {
enum {Q_SIZE = 10};
class Node{
public:
Item item;
Node * next;
Node(const Item & it):item(it),next(nullptr){};
};
Node *front;
Node *end;
int items;
const int qsize;
Queue(const Queue &queue){};
Queue &operator=(const Queue &queue);
public:
Queue(int qs= Q_SIZE);
~Queue();
bool isempty() const;
bool isfull() const;
int queuecount() const;
bool add(const Item &item);
bool get(Item &item);
};
异常:
调用abort()
#include <iostream>
#include <cstdlib>
using std::cout;
using std::cin;
using std::endl;
double hmean(double a, double b);
int main(int argnum, char *args[]) {
cout << hmean(12,3) << endl;
hmean(3,-3);
cout << "Done.\n";
return 0;
}
double hmean(double a, double b){
if (a == -b){
std::cout << "a,b val error. abort()" << endl;
std::abort();
}
return 2.0 * a * b / ( a + b );
}
结果
4.8
a,b val error. abort()
在hmean中调用abort()将直接终止程序。
异常机制:
对异常处理有三个组成部分:
- 引发异常 throw
- 使用处理程序捕获异常
- 使用 try -catch
error3.cpp
#include <iostream>
#include <cstdlib>
using std::cout;
using std::cin;
using std::endl;
double hmean(double a, double b);
int main(int argnum, char *args[]) {
cout << hmean(12,3) << endl;
try {
hmean(3,-3);
}catch (const char *s){
cout << s << endl;
}catch (const int s){
cout << s << endl;
}catch (...){
cout << "catch all error" << endl;
}
cout << "Done.\n";
return 0;
}
double hmean(double a, double b){
if (a == -b){
throw "a,b val error. abort()";
}
return 2.0 * a * b / ( a + b );
}
结果
4.8
a,b val error. abort()
Done.
throw语句类似返回语句,因为它也将终止函数的执行。但throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。
catch块表明这是一个处理函数,char *s则表明处理该程序与字符串异常匹配。
执行完try模板时,如果没有引发任何异常,则程序调到catch块后继续执行。
catch (…) 可以捕获全部异常
exception类:
exception 头文件定义了exception类,C++可以把它作为其他异常的基类。代码可以引发exception异常,也可以将exception作为基类,有一个名为what()的虚函数,它返回一个字符串。由于是虚方法,可以在派生中重新定义它:
#include <exception>
class bad_hmean:public std::exception{
public:
const char * what(){ return "bad argument to heman"}
};
c++ 库定义了很多基于exception的异常类型。
stdexcept异常类:
头文件stdexcept定义了其他几个异常类。首先,该文件定义了logic_error 和 runtime_error类,它们都是以公有方式从exception派生而来的:
class logic_error: public exception{
public:
explicit logic_error(const string & what_arg);
......
};
class runtime_error: public exception{
public:
explict runtime_error(const string & what_arg);
......
};
这些类的构造函数接受一个string对象作为参数,该参数提供了方法what()以C-风格字符串方式返回的字符数据。
这两个新类被用作两个派生类系列的基类。异常类系列logic_error描述了典型的逻辑错误。总体而言,通过合理编程可以避免这种错误,但实际上还是有可能发生:
-
domain_error:
数学函数有定义域(domain)。如果编写一个函数,将参数传递给std::sin(),则可以让函数在参数不在定义域-1到1之间时引发异常
-
invalid_argument:
指出给函数传递了一个意料之外的值。,例如,函数希望接受一个这样的字符串:其中每个字符要么是’0’要么是‘1’,则当传递的字符串中包含其他字符时,该函数可以引发异常。
-
length_error:
用于指出没有足够的空间来执行所需要的操作。如string类中的append()方法在合并得到的字符串长度超过最大允许长度时,将引发异常
-
out_of_bounds:
指名索引错误,如定义一个数组,其operator(int n)[]在使用索引无效时引发异常
runtime_error 异常系列描述了可能在运行期间发生,但难以预计和防范的错误:
- range_error
- overflow_error
- underflow_error
下溢(underflow)错误在浮点数计算中。一般而言,存在浮点类型可以表示的最小非零值,计算结果比这个值还小将导致下溢错误。整型和浮点型都可能发生上溢错误。计算结果不再函数允许范围内,但没有发生上溢或下溢,这时,可以使用range_error
bad_alloc 和 new:
对于使用new导致的内存分配问题,C++最新处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的声明,它是从exception类公有派生而来的:
newexcp.cpp
#include <iostream>
#include <cmath>
const int length = 120000;
using std::cout;
using std::endl;
struct Big{
double stuff[20000];
};
int main(){
Big * pb;
try{
pb = new Big[length];
cout << "past new,size: ";
cout << sizeof(*pb)* length /1024/1024/1024<< endl;
}catch (std::bad_alloc &al){
cout << al.what() << endl;
}
return 0;
}
异常何时会迷失方向:
未捕获异常不会导致程序立即停止。相反,程序首先调用terminate()函数。默认情况下terminate()函数将调用abort()函数。可以指定terminate()函数应调用的函数来修改terminate()的这种行为。为此,可调用set_terminate()函数。两个函数都是在头文件exception中声明的
typedef void (*teminate_handler)();
teminate_handler set_terminate(teminate_handler f) noexcept;
void terminate() noexcept;
其中typedef使得teminate_handler成为这样一种类型的名称:指向没有参数和返回值的函数的指针。set_terminate()函数将不带任何参数且返回值为void的函数的名称(地址)作为参数,并返回该函数的地址。如果多次调用set_terminate(),将使用最后一次设置的函数
#include <iostream>
#include <cmath>
#include <exception>
const int length = 1200000;
using std::cout;
using std::endl;
struct Big{
double stuff[20000];
};
void myQuit();
int main(){
std::set_terminate(myQuit);
Big * pb;
try{
pb = new Big[length];
cout << "past new,size: ";
cout << sizeof(*pb)* length /1024/1024/1024<< endl;
}catch (const char *s){
cout << s << endl;
}
return 0;
}
void myQuit(){
cout << "Terminating due to uncacught exception.\n";
exit(5);
}
RTTI运行阶段标识:
RTTI的用途:
假设有一个类层次结构其中的类都是从同一个基类派生而来的,则可以让基类的指针指向其中任何一个类的对象。这样便可以调用这样的函数:在处理一些信息后,选择一个类,并创建这种类型的对象,然后返回它的地址,而该地址可以被赋给基类指针。如何知道指针指向的是哪种对象?
RTTI的工作原理:
C++有3个支持RTTI的元素:
- 如果可能的话dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回0-空指针
- typeid运算符返回一个指出对象的类型的值
- type_info 结构存储了有关可定类型的信息
只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才能将派生对象的地址赋给基类指针
dynamic_cast运算符:
dynamic_cast运算符是最常用的RTTI组件,它不能回答“指针指向的是哪类对象”的问题,但能够回答“是否可以安全地将对象的地址赋值给特类型的指针”这样的问题
Superb *pm = dynammic_const<Superb *>(pg);
如果pg的类型可以安全的转换成Superb就返回对象地址,否则返回空指针。
rtti1.cpp
#include <iostream>
#include <cstdlib>
#include <ctime>
using std::cout;
class Grand
{
private:
int hold;
public:
Grand(int h = 0) :hold(h){};
virtual void Speak() const { cout << "I am a grand class!\n";}
virtual int Value() const { return hold;}
};
class Superb :public Grand
{
public:
Superb(int h = 0):Grand(h){}
void Speak() const { cout << "I am a superb class!\n";}
virtual void Say() const { cout << "I hold the superb value of " << Value() << "!\n";}
};
class Magnificent :public Superb
{
private:
char ch;
public:
Magnificent(int h=0,char c = 'A'):Superb(h),ch(c){}
void Speak() const { cout << "I am a magnificent class!\n";}
void Say() const { cout << "I hold the character " << ch <<
" and the integer " << Value() << "!\n";}
};
Grand * GetOne();
int main(){
std::srand(std::time(0));
Grand * pg;
Superb * ps;
for (int i = 0;i < 5;i++){
pg = GetOne();
pg->Speak();
if(ps = dynamic_cast<Superb *>(pg)){
ps->Say();
}
delete pg;
}
return 0;
}
Grand * GetOne(){
Grand * p;
switch (std::rand() % 3){
case 0: p = new Grand(std::rand() % 100);
break;
case 1: p = new Superb(std::rand() % 100);
break;
case 2: p = new Magnificent(std::rand() % 100,'A' + std::rand() % 26);
break;
}
return p;
}
结果
I am a superb class!
I hold the superb value of 40!
I am a superb class!
I hold the superb value of 43!
I am a magnificent class!
I hold the character I and the integer 53!
I am a magnificent class!
I hold the character H and the integer 46!
I am a grand class!
程序说明,只为superb和magnificent类调用了Say()方法。
也可将dynamic_cast用于引用,其用法稍有不同;没有与空指针对应的引用值,因此无法使用特殊的引用值来表示失败。当请求不正确时,dynamic_cast将引发bad_cast异常,这种异常是从exception派生出来的,它是在typeinfo中定义的。因此,可以像下面这样使用该运算符,其中rg是对Grand对象的引用。
#include<typeinfo>
...
try{
Superb & rs = dynamic_cast<Superb &>rb;
....
}catch(bad_cast &){
...
}
typeid运算符和type_info类:
typeid运算符使得能够确定两个对象是否为同种类型。它与sizeof有些相似,可以接受两种参数:
- 类名
- 结果为对象的表达式
typeid运算符返回一个type_info对象的引用,其中,type_info是在头文件typeinfo中定义的一个类。type_info类重载了==和!=运算符,以便可以使用这些运算符来对类型进行比较。例如,如果pg指向的是一个Magnificent对象,则下述表达式的结果bool值为true否则为false:
typeid(Magnificent) == typeid(*pg)
如果pg是一个空指针,程序将引发bad_typeid异常。该异常是从exception类派生而来的,是在头文件typeinfo中声明的。
type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串:通常(但并非一定)是类的名称。例如,下面的语句显式指针pg指向对象所定义的类定义的字符串。
cout << "Now processing type " << typeid(*pg).name() << ".\n";
rtti2.cpp
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <typeinfo>
using std::cout;
class Grand
{
private:
int hold;
public:
Grand(int h = 0) :hold(h){};
virtual void Speak() const { cout << "I am a grand class!\n";}
virtual int Value() const { return hold;}
};
class Superb :public Grand
{
public:
Superb(int h = 0):Grand(h){}
void Speak() const { cout << "I am a superb class!\n";}
virtual void Say() const { cout << "I hold the superb value of " << Value() << "!\n";}
};
class Magnificent :public Superb
{
private:
char ch;
public:
Magnificent(int h=0,char c = 'A'):Superb(h),ch(c){}
void Speak() const { cout << "I am a magnificent class!\n";}
void Say() const { cout << "I hold the character " << ch <<
" and the integer " << Value() << "!\n";}
};
Grand * GetOne();
int main(){
std::srand(std::time(0));
Grand * pg;
Superb * ps;
for (int i = 0;i < 5;i++){
pg = GetOne();
cout << "Now processing type " << typeid(*pg).name() << ".\n";
pg->Speak();
if(ps = dynamic_cast<Superb *>(pg)){
ps->Say();
}
if (typeid(Magnificent) == typeid(*pg)){
cout << "Yes, your're really magnificent.\n";
}
}
return 0;
}
Grand * GetOne(){
Grand * p;
switch (std::rand() % 3){
case 0: p = new Grand(std::rand() % 100);
break;
case 1: p = new Superb(std::rand() % 100);
break;
case 2: p = new Magnificent(std::rand() % 100,'A' + std::rand() % 26);
break;
}
return p;
}
结果
Now processing type 5Grand.
I am a grand class!
Now processing type 5Grand.
I am a grand class!
Now processing type 11Magnificent.
I am a magnificent class!
I hold the character Q and the integer 60!
Yes, your're really magnificent.
Now processing type 5Grand.
I am a grand class!
Now processing type 6Superb.
I am a superb class!
I hold the superb value of 38!
类型转换运算符:
由于C中类型转换限制松散,更加严格地限制允许转换的类型:
-
dynamic_cast (dynamic 动态的) 用于将类向上转换
-
const_cast
const_cast 运算符用于执行只有一种用途的类型转换,即改变值为const 或 volatile,如果类型的其他方面也被修改,这转换将出错。
High bar; const High * pbar = &bar; High * pb = const_cast<High *>(pbar) // valid const Low * pl = const_cast<Low *>(pbar) // invalid
-
static_cast
static_cast<type_name>(expression)
仅当type_name可被隐式转换为expression所属的类型或expression可被隐式转换为type_name所属的类型时,上述转换才是合法的,否则将出错。假设High是Low的基类,而Prod是无关类,则从High到Low、从Low到High的转换都是合法的,而从Low到Prod的转换是不允许的
-
reinterpret__cast (reinterpret 重新解读)
reinterpret__cast用于天生危险的类型转换。
struct dat {short a; short b;}; long value = 0xA224B118; dat * pd = reinterpret__cast<dat *>(&value); cout << hex << pd->a;