第十五章:友元,异常和其它
前言:本章有三个主要内容,分别是友元,异常和其它;本章除了进一步深入剖析友元的使用(不仅仅是友元函数,还包括友元类),还会提供关于异常的处理方式(这对大型程序的稳定性十分重要),另外还会额外讨论一些其它知识;
1.友元
友元函数可用于扩展类的接口,而友元类的所有方法则都可以使用被友元类的数据成员,包括公有私有保护等所有成员;
在一个类中将另一个类声明为友元,只需在一个类中添加如下语句:
friend class Remote;
//友元声明对位置不敏感,在公有,私有,保护部分使用的效果是一样的
实际上,可能只需要将类中的某个成员函数解析为友元,那么可以借助成员运算符:
class Tv
{
friend void Remote::set_chan(Tv & t, int c);
};
另外,将类声明为友元面临一个问题:需要将类声明本体放置在包含类的前面;当不想这么做或者两个类互相为对方的友元时,可以借鉴处理未定义但先使用的函数的方法:在使用它的代码前加一个声明性质的语句:
class Tv;
//这种技术有它自己的名字:前向声明
2.嵌套类
在C++中,可以将一个类声明放在另一个类中,这样的处理方式称为嵌套类;然而这和包含一个类的性质是不同的,前者只是包含了类的定义,并不是包含某个具体的类;并且前者的成员方法还可以自己创建嵌套类的实例;
如果想要在程序的其它地方使用嵌套类来创建实例,则需要使用作用域解析运算符:
Team::Coach forhire;
//Coach是嵌套在Team中的
下面的表格总结了嵌套类,结构和枚举的作用域结构特征:
声明位置 | 包含它的类是否可以使用它 | 从它派生而来的类是否可以使用它 | 在外部是否可以使用 |
---|---|---|---|
私有部分 | 是 | 否 | 否 |
保护部分 | 是 | 是 | 否 |
公有部分 | 是 | 是 | 是,通过类限定符来使用 |
另外嵌套类还可以出现在模板中,不会导致额外的问题;
3.异常
异常是相对较新的C++功能,这意味着在某些较老的编译器中不得到支持,在某些较新的编译器中此选项是默认关闭的;
处理方法之一是:调用abort()函数;这个函数位于头文件或<stdlib.h>中;作用是向标准错误流发送abnormal program termination(程序异常终止)消息,然后终止程序,还返回一个随实现而异的值(如果程序是由另一个程序调用的,则告诉父进程);通知操作系统处理失败;是否刷新文件缓冲区也取决于实现;如果愿意,也可以使用exit(),该函数刷新文件缓冲区但不显示信息;
也可以使用一个全局变量来存储可能发生异常时的特征值,但务必确保这个值没有被挪作他用;
对异常的处理一般分三个部分:
- 引发异常
- 使用处理程序捕获异常
- 使用try块
throw语句是向catch块抛出错误信息,并匹配合适的处理块;该关键字后的值指出了异常的特征;该异常特征会尝试和catch块括号中的变量匹配,将进入合适的处理块进行错误处理;处理完成后继续到处理块序列下第一条常规语句中开始执行;
try块后面有一个花括号,表示其中特定的异常可能激活的代码,后面跟有一个或多个catch块;
下面让我们看一个简单的例子:
#include<iostream>
double hmean(double a, double b);
int main()
{
double x, y;
std::cout<<"Enter teo numbers";
while (std::cin>> x >> y)
{
try {
z = hmean(x, y);
}
//可能发生异常的代码
catch (const char * s)
{
std::cout << s <<std::endl;
std::cout<< "Enter a new pair of numbers:";
continue;
}
//用于处理异常的代码
std::cout<< "Harmonic mean of" << x << "and" << y << "is" << z << std::endl;
std::cout<<"Enter newt set if numbers <q to quit>:";
}
std::cout << "Bye!\n";
return 0;
}
double hmean(double a,double b)
{
if (a == -b)
throw "bad hmean() arguments:a = -b not allowed";//抛出一个异常等待和某个catch块匹配
return 2.0 * a * b / (a + b);
}
我们可以看到try块中包含了一条语句,如果块中的语句抛出了异常,那么后面的catch块会对异常进行处理;但如果程序在try块外面调用hmean()
,抛出的异常是无法被处理的;throw()
函数和return
类似,也中止函数的执行,但前者会沿着调用的路径回溯,直到遇到try块为止,若始终无法回溯到try块中,则这个异常不会得到处理;
如果try块中的语句没有引发异常,那么程序会跳过后面catch块的内容直接向后执行;另外,在引发异常但没有提供解决方案时,程序会默认调用abort()
函数;
通常,引发异常的函数会传递一个保存异常信息的对象,并尝试和catch块匹配;另外,基类的引用默认可以指向继承类,这也就是说当我们使用引用(实际指针也有相同的性质)匹配异常时,可能会发生某个保存信息的类和它的基类引用匹配的情况,解决方法时按照继承的顺序,恰好反过来排列处理不同对象的catch块,这样就能保证每一种位于继承链中的异常类都能恰好匹配到自己的catch块;
C++11中有一种称为异常规范的语法,虽然饱受争议但我们仍应该了解:
double harm(double a) throw(bad_thing);//表示这个函数将可能主动抛出bad_thing异常
double marm(double b) throw();//表示目前没有发现这个函数可能抛出哪些异常
//throw关键字只表明可能抛出哪些异常,不声明不可能抛出异常
另外,还有另一种关键字:
double marm() noexept;//表示这个函数不可能抛出异常
当一个函数本身没有引发异常,但它调用的函数可能引发异常,或者下游调用链的某个函数可能引发了异常,而该函数又位于try块中时,就会引发栈解退,也就是说函数调用链会回溯,直到遇到第一个拥有try操作的语句块,然后向下面的catch块匹配,试图进行异常处理;不同的是,对于相关标记异常信息的对象,虽然理论上讲应当从栈中弹出,但为了保存异常信息实际上其析构函数并不会被调用;
在catch块中可能异常处理后还会再抛出异常,这样的异常会由下一个try-catch块捕获并进行处理;如果没有找到这样的处理程序,则默认情况下程序将异常终止;
有一个比较新奇的特性,那就是引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用;
有时候我们不知道所有的异常情况,这时可以使用省略号来概括表示所有异常情况:
catch (...) {//异常处理语句}
可以像这样写一个异常匹配的catch块序列:
try{
duper();
//这个函数可能抛出下面catch块将处理的异常
}
catch(bad_3 &be)
{//某个处理方案}
catch(bad_2 &be)
{//某个处理方案}
catch(bad_1 &be)
{//某个处理方案}
catch(bad_hmean & h)
{//某个处理方案}
catch(...)
{//某个处理方案}
C++有一个exception头文件,其中定义了一个名为exception的类,这个类有有一个名为what()
的虚函数,返回一个字符串。该字符串的特征随实现而异;在exception的派生类中,可以对what()函数进行重载,来让它显示不同的错误信息(这就是刚刚提到的“特征随具体实现而异”);
这里让我们实际使用一下这个类:
#include <exception>
class bad_hmean : public std::exception
{
public:
const char * what() {return"bad arguments to hmean()";}
....
};
class bad_gmean : ublic std::exception
{
public:
const char * what() {return"bad argument to gmean()";}
};
try{
...
}
catch(std::exception & e)//两种异常在同一个catch块中处理
{
cout << e.what() << endl;
...
}
头文件stdexcept定义了其它几个异常类,首先该文件定义了logic_error和runtime_error类,它们都是公有方式从exception派生得来的;这两个类的构造函数都接收一个string类作为参数,提供了方法what()以C风格字符串方式返回的字符数据类型;
logic_error派生出了这些派生类:
- domain_error:传递给函数的参数不在定义域中
- invalid_argument:给函数传递了一个意料外的值
- length_error:没有足够的空间来执行所需要的操作
- out_of_bounds:索引错误
runtime_error派生出了这些派生类:
- range_error:计算结果不在函数允许范围之内,虽然没有发生上溢或下溢
- overflow_error:发生上溢
- underflow_error:发生下溢
这些类中,每个都有一个自己的what()
函数,可以供返回字符串;另外补充一点,new申请内存空间失败时将引发bad_alloc
异常;
但在曾经的实现中,申请内存空间失败时会返回一个空指针(实际上我认为这种处理方法也不赖);它(bad_alloc
)也是从exception类公有派生来的;
当然,现在的实现也并不是完全取消了申请内存空间失败时的new,这种new在申请失败时返回空指针:
int * pi = new (std::nothrow) int;
int * pa = new (std::nothrow) int[500];
异常被引发后,有两种可能会遗失方向;在带异常规范的函数中引发但没有和处理块匹配称为意外异常,在捕获区域外抛出的异常称为未捕获异常;这两种情况都会导致程序异常终止;但我们可以定制程序面对这两种情况时的反应:
程序在遇到这两种意外情况时会调用函数
terminate()
,而该函数默认调用abort()
,但我们可以使用函数set_terminate()
来定制terminate()
函数的行为,直接将函数的内存地址作为参数传入set_terminate()
函数即可;
下面让我们来动手实现一次对默认处理函数的修改:
#include <exception>
using namespace std;
void myQuit()
{
cout << "Terminating due to uncaught excetion\n";
exit(5);
}
set_terminate(myQuit);
...//下面的程序将在遇到未被合适处理的异常时,调用我们自定义的处理函数
上面的情况是针对函数合理的抛出了异常,但没有被捕获导致的,下面的情况是由函数抛出了预料之外的异常导致的:程序调用unexpected()
函数,默认调用abort()
函数终止程序;当然类似的是该函数也可以使用set_unexpected()
函数来修改默认调用的函数,这里就不再赘述了;
但这种情况是特殊的并且限制更严格,具体而言该情况的异常处理函数可以:
- 通过调用terminate(),abort(),exit()来终止程序
- 引发异常
引发异常(第二种选择)的结果取决于unexpected_handler函数所引发的异常以及引发意外异常的函数的异常规范:
-
如果新引发的异常和原来的异常规范匹配,则程序将从那里开始进行正常处理即寻找与新引发异常匹配的catch块;基本上这种方法将用预期的异常取代意外的异常;
-
如果新引发的异常和原来的异常规范不匹配,且异常规范中没有包括std::bad_expection类型,则程序将调用terminate();bad_exception是从exception派生而来的,其声明头文件是exception;
-
如果新引发的异常与原来的异常规范不匹配,且原来的异常规范中包含了std::bad_exception类型,则不匹配的异常将被std::bad_exception异常所取代
下面让我们将所有捕获的意外异常都替换为预期异常:
#include<exception>
using namespace std;
void myUnexpected()
{
throw std::bad::exception;
}
set_unexpected(myUnexpected);
另外要注意内存管理和异常并不是总能协调工作;要正确释放那些存储在堆中的数据,应当在异常处理中添加释放内存的代码;
4.RTTI
RTTI指的是运行过程类型识别,这虽然是一种包含争议的C++特性,但我们不去研究谁对谁错只讨论这项技术本身的特性;另外虽然某些不同厂家提供的库中有这项功能,但往往各自的实现机制不同;
RTTI元素一共有三个:
- 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个派生类的指针,否则该运算符返回空指针;
- typeid运算符返回一个指出对象类型的值
- type_info结构存储了有关特定类型的信息
dynamic_cast的使用方法:
Superb * pm = dynamic_cast<Superb *>(pg);
//若成功将完成赋值,失败则得到空指针
注意:即使编译器支持RTTI,它也可能是默认关闭的;另外,还可以将dynamic_cast运算符用于引用:
Superb & pm = dynamic_cast<Superb &>(pg);
typeid和typeof有些相似,它接收这两种参数:
- 类名
- 结果为对象的表达式
该运算符返回一个type_info对象的引用,后者是在头文件typeinfo中定义的;另外type_info还重载了运算符==和!=,可以用于进行比较;如果使用typeid()对一个空指针进行运算则会抛出一个bad_typeid异常,该异常从exception类中派生而来,在头文件typeinfo中声明;
5.类型转换运算符
在C++中有这样四种类型转换运算符:
- dynamic_cast
- const_cast
- static_cast
- reinterpret_cast
其中,dynamic_cast的使用方法是:
pl = dynamic_cast< type_name * >(expression);
//只能用于将基类或指向基类的引用或指针转换为指向派生类的
另外,const_cast的用法相似,但是只能用于const和volatile的转化;而static_cast则相对自由可以支持更多的类型;reinterpret_cast则用于危险的转换,这类转换往往由底层提供支持,不可移植也并不通用;