2、抛出异常可通过throw表达式来实现,throw表达式看起来非常像return语句,throw表达式由关键字throw后面跟一个表达式构成,该表达式的类型是被抛出异常的类型。例如:
//异常类
class popOnEmpty{/* ... */};
class pushOnFull{/* ... */};
throw popOnEmpty ; //这样不是完全正确
因为异常是一个对象,我们在抛出异常时应该抛出一个异常类的对象。所以在throw表达式中的表达式不能只是一个类型,而是应该是一个类对象。为了创建类对象,我们需要调用类的构造函数。所以正确的throw表达式如下:
throw popOnEmpty() ; //这样就完全正确
该throw表达式创建一个popOnEmpty类型的异常对象。
3、虽然异常往往是class类型的对象,但是throw表达式也可以抛出任何类型的对象,例如,可以抛出类型是枚举类型的异常。
enum EHstate{noErr,zeroOp,negativeOp,severeError};
int mathFunc(int i)
{
if (i == 0)
{
throw zeroOp ;// 枚举类型的异常
}
//否则的话,继续正常处理流程
}
4、try块
try块可以包含任何C++语句-——表达式以及声明。一个try块引入一个局部域,在try块内声明的变量不能在try块外被引用,包括在catch字句中。例如:
int main()
{
try
{ //引入了一个try块的局部域
iStack stack(32) ; //在try块中声明
stack.display() ;
for (int i=1;i<51;++i)
{
if (i % 3 == 0)
stack.push(i) ;
if(i % 4 == 0)
stack.display() ;
if (i % 10 == 0)
{
int dummy ;
stack.pop(dummy) ;
stack.display() ;
}
}
}
catch(pushOnFull){
//这里不能引用stack
}
catch(popOnEmpty){
//这里不能引用stack
}
//这里(main函数的域)不能引用stack
return 0 ;
}
5、函数try块
我们如何放置try块的位置呢?我们可以把整个函数体包含在一个函数try块中。这种组织结构把程序的正常处理代码和异常处理代码分离的最为清楚。例如:
int main()
try //函数try块
{ //引入了一个try块的局部域
iStack stack(32) ; //在try块中声明
stack.display() ;
for (int i=1;i<51;++i)
{
if (i % 3 == 0)
stack.push(i) ;
if(i % 4 == 0)
stack.display() ;
if (i % 10 == 0)
{
int dummy ;
stack.pop(dummy) ;
stack.display() ;
}
}
return 0 ;
}
catch(pushOnFull){
//这里不能引用stack
}
catch(popOnEmpty){
//这里不能引用stack
}
从上面的例子中,我们可以看到:关键字try在函数体的开始花括号之前,catch字句列在函数体结束花括号之后。通过这种组织方式,main()中正常处理的代码被放在函数体中,与catch字句中处理异常的代码清楚的分开。但是,在main的函数体中声明的变量不能在catch字句中被引用。
6、catch字句
catch字句用于捕获异常。一个catch字句由三部分构成:关键字catch,在括号中的单个类型或单个对象声明(被称作异常声明——用于表示该catch字句处理的异常的类型)以及复合语句中的一组语句。例如:
catch(pushOnFull)//pushOnFull是异常声明,且是异常类型的声明
{
cerr << "trying to push a value on a full stack\n" ;
return errorCode88 ;
}
catch(popOnEmpty)//popOnEmpty是异常声明,且是异常类型的声明
{
cerr << "trying to pop a value on an empty stack\n" ;
return errorCode89 ;
}
上面的两个catch字句都有一个class类型的异常声明:第一个是pushOnFull类型,第二个是popOnEmpty类型。
7、在程序执行完catch字句的列表的最后字句时,程序将在其后继续执行。具体可以参考下面的例子:
int main()
{
iStack stack(32) ;
try
{
stack.display() ;
for (int i=1;i<51;++i)
{
//同前
}
}
catch(pushOnFull)//pushOnFull是异常声明,且是异常类型的声明
{
cerr << "trying to push a value on a full stack\n" ;
return errorCode88 ;
}
catch(popOnEmpty)//popOnEmpty是异常声明,且是异常类型的声明
{
cerr << "trying to pop a value on an empty stack\n" ;
return errorCode89 ;
}
//程序在这里继续执行
return 0 ;
}
C++的异常处理机制被称为是不可恢复的:一旦异常被处理,程序的执行就不能够在异常被抛出的地方继续。
8、catch字句括号里面的异常声明可以是类型声明或一个对象声明。什么时候catch字句中的异常声明应该声明一个对象呢?
当我们要获得throw表达式的值,或者要操纵throw表达式所创建的异常对象时,我们应该声明一个对象。假设我们设计自己的异常类,当该异常被抛出时,我们把信息存储在异常对象中,如果catch字句的异常声明声明了一个对象,则catch字句中的语句就可以用该对象来引用由throw表达式存储的信息。具体可以看下面的例子:
//异常类
class pushOnFull
{
public:
pushOnFull(int i) : _value(i){ }
int value(){return _value ;}
private:
int _value ;
};
void iStack::push(int value)
{
if (full())
//把value存储在异常对象中
throw pushOnFull(value) ;
//...
}
//下面的catch字句。注意其括号中的改变
catch(pushOnFull obj)
{
cerr << "trying to push a value "<< obj.value()
<< "a full stack\n" ;
}
注意,catch字句的异常声明声明了对象obj,用它来调用pushOnFull类的成员函数value()。
9、异常对象总是在抛出点被创建,即使throw表达式不是一个构造函数调用,或者它没有表现出要创建一个异常对象,情况也是如此。例如:
enum EHstate{noErr,zeroOp,negativeOp,severeError};
enum ENstate state = noErr ; //全局域
int mathFunc(int i)
{
if(i == 0)
{
state = zeroOp ;
throw state ; //创建异常对象
}
}
在这里例子中,对象state没有被用作异常对象,而是由throw表达式创建了一个类型为EHstate的异常对象(该异常对象不同于对象state),并且用全局对象state的值初始化该对象。即也就是说明异常对象总是在抛出点被创建。如果不是在抛出点创建的话,对象state在全局域中已经创建了。即也就是说对象不是在抛出点创建的。但是,现在这里对象state没有被用作异常对象。所以,异常对象总是在抛出点处创建。
10、catch字句的异常声明的行为特别像参数声明。例如:
void calculate(int op)
{
try
{
mathFunc(op) ;
}
catch(EHstate eObj){
//eObj是被抛出的异常对象的拷贝
}
}
这个例子的catch字句中的异常声明类似于按值传递参数。对象eObj用该异常对象的值初始化,就好像一个按值传递的函数参数用相应实参的值初始化一样。
11、catch字句的异常声明可以是引用声明
这样的话,catch字句就可以直接引用由throw表达式创建的异常对象(注意是throw表达式创建的异常对象,不是全局域中用于初始化throw表达式创建的异常对象的对象,例如,前面将的state),而不是一个局部拷贝了。例如:
void calculate(int op)
{
try
{
mathFunc(op) ;
}
catch(EHstate &eObj){
//eObj引用了被抛出的异常对象,注意不是用于初始化被抛出的异常对象的对象
}
}
为了防止不必要的拷贝大型类对象,class类型的参数应该被声明为引用。同样的原因,如果class类型异常的异常声明被声明为引用,也是比较好的。
12、catch-all字句
//对任何异常都会进入
catch(...){
//这里是我们的代码
}
注意点:与其他的catch字句联合使用时,它必须总是被放在异常处理代码表的最后,否则会产生一个编译时刻错误。
13、异常规范
异常规范提供了一种方案,它能够随着函数声明列出该函数可能抛出的异常,它保证该函数不会抛出任何其他类型的异常。异常规范跟在函数参数表之后,它用关键字throw来指定,后面是用括号括起来的异常类型表。例如:
class iStack
{
public:
//...
void pop(int &value) throw(popOnEmpty) ; //使用了异常规范throw(popOnEmpty)
void push(int &value) throw(pushOnFull) ;//使用了异常规范throw(pushOnFull)
private:
//...
};
对于pop函数的调用,保证不会抛出任何popOnEmpty类型之外的异常。
一个函数的异常规范的违例只能在运行时刻才能被检测出来。编译时刻不会检测出来。如果函数抛出了一个没有被列在其异常规范中的异常,则系统调用C++标准库中定义的函数unexpected(),unexpected()的缺省行为是调用terminate()。
14、异常规范的注意点
01、空的异常规范保证函数不会抛出任何异常。例如:
extern void no_problem() throw() ;
该函数不会抛出任何异常
02、如果一个函数声明没有指定异常规范,则该函数可以抛出任何类型的异常
extern void any_problem() ;
该函数会抛出任何异常
03、在被抛出的异常类型与异常规范中指定的类型之间不允许类型转换。例如:
int convert(int parm) throw(string)
{
//...
if (somethingRather)
//程序错误:
//convert()不允许const char*型的异常
throw("Hello") ;
}
函数convert中的throw表达式抛出一个C风格的字符串。由于这个throw表达式创建的异常对象的类型为const char*。通常,const char*型的表达式可以被转换成string类型。但是在这里,异常规范不允许从被抛出的异常类型到异常规范指定的类型之间的转换。如何修正呢?
throw string("Hello") ;
15、异常规范和函数指针
我们可以在函数指针的声明处给出一个异常规范。例如:
void (*pf)(int) throw(string) ;
当带有异常规范的函数指针被初始化时,对于用作初始值的指针类型有一些限制。这两个指针的异常规范不必完全一样。但是吗,用作初始值或右值的指针的异常规范必须与被初始化或赋值的指针的异常规范一样活更严格。例如:
void recoup(int,int) throw (exceptionType) ;
void no_problem() throw() ;
void doit(int,int) throw(string,exceptionType) ;
//正确:recoup与pf1的异常规范一样
void (*pf1)(int,int) throw(exceptionType) = &recoup ; //使用的是函数名加取地址符
//或使用如下格式也是可以的
void (*pf1)(int,int) throw(exceptionType) = recoup ; //使用的是函数名,二者是一样的
//正确:no_problem比pf2更严格
void (*pf2)() throw(string) = &no_problem ;
//错误:doit没有pf3严格
void (*pf3)(int,int) throw(string) = &doit ;
16、rethrow表达式
rethrow表达式的形式如下:
throw;
请看下面的一个例子:
void calculate(int parm)
{
try
{
mathFunc(parm) //抛出divideByZero异常
}
catch (mathExcp mExcp)
{
//部分地处理当前异常
//并重新抛出该异常对象
throw ;
}
}
被重新抛出的异常类型是mathFunc()抛出的异常类型(即divideByZero),或者是在catch子句的异常声明中的类型(即mathExcp)吗?
请记住:throw表达式重新抛出的异常是原来的异常对象。由于原来的异常对象的类型是divideByZero,所以重新抛出的异常也是divideByZero。