7.2 异常处理--Exception Handling
编译器主要工作是找出catch子句,以处理抛出来exception。
这需要追踪程序堆栈中每一个函数的当前作用区域。同时,编译器必须提供某种查询exception objects的方法,以知道其实际类型(导致某种形式的执行期类型识别,也就是RTTI)。最后,还需要某种机制用以管理被丢出的object,包括它的产生,储存,可能的解构,清理以及一般存取。也有可能有一个以上的objects同时起作用。一般而言,exception handling机制需要与编译器所产生的数据结构以及执行期的一个exception library紧密合作。 在程序大小和执行速度之间,编译器必须有所选择:
Ⅰ、为了维持执行速度,编译器可以在编译期建立起用于支持的数据结构.这会使程序膨胀,但编译器可以几乎忽略这些结构,直到有个exception被丢出来。
Ⅱ、为了维护程序大小,编译器可以在执行期建立用于支持的数据结构.这会影响程序的执行速度,但意味着编译器只有在必要的时候才建立那些数据结构。
Exception Handling快速检阅
C++的exception handling由三个主要的语汇组件构成:
1.一个 throw 子句:它在程序某处发出一个exception.被丢出去的exception可以是内建类型,也可以是使用者自定类型。
2.一个或多个 catch 子句:每一个 catch 子句都是一个exception handler。它用来表示说,这个子句准备处理某种类型的exception,并且在封闭的大括号区段中提供实际的处理程序。
3.一个 try 区段:它被围绕以一系列的叙述句,这些叙述句可能会引发 catch 子句起作用。
当一个exception被抛出去时,控制权会从函数调用中被释放出来,并寻找一个吻合的 catch 子句.如果都没有吻合者,那么默认的处理例程terminate()会被调用。当控制权被放弃后,堆栈中的每一个函数调用也就被推离.这个程序称为unwinding the stack。在每一个函数被推离堆栈之前,函数的local class objects的destructor会被调用。
在程序员层面,exception handling也改变了函数在资源管理器上的语意。例如,下面的函数中含有对一块共享内存的locking和unlocking操作.虽然看起来和exceptions没有什么关联,但在exception handling下并不保证能够正确运行:
void mumble(void *arena)
{
Point *p = new Point;
smLock(arena); // function call
// 如果有一个exception在此发生,问题就来了
// ...
smUnlock(arena); // function call
delete p;
}
本例中,exception handling机制把整个函数视为单一区域,无需操心"将函数从程序堆栈中'unwinding'"的事情。然而从语意上说,在函数被推出堆栈之前,需要unlock共享内存,并 delete p。让函数成为"exception proof"的最明确的方法就是插入一个default catch 子句,像这样:
void mumble(void *arena)
{
Point *p;
p = new Point;
try{
smLock(arena);
// ...
} catch (...) {
smUnlock(arena);
delete p;
throw;
}
smUnlock(arena);
delete p;
这个函数现在有了两个区域:
1.try block以外的区域,在那里,exception handling机制除了"pop"程序堆栈之外,没有其他事情要做.
2.try block以内的区域(以及它所联合的default catch 子句)
请注意,new 运算符的调用并非在 try 区段,这是错误吗?
如果 new 运算符在Point constructor配置内存后发生一个exception,那么内存既不会被unlocking,p也不会被 delete,是不是这样?
①、如果 new 运算符抛出一个exception,那么就不需要配置heap中的内存,Point constructor也不需要被调用,所以也就没有理由调用 delete 运算符。②、如果是在Point constructor中发生exception,此时内存已配置完成,那么Point中任何构造好的合成物或子对象都将自动被解构,然后heap内存也会释放掉。无论哪种情况,都不需要调用 delete 运算符.
同理,如果一个exception是在 new 运算符执行过程中被丢出,arena所指向的内存就绝不会被locked,因此,也没有必要unlock。
处理这些资源管理问题,一个好的办法是将资源需求封装于一个 class object体内,并由destructor来释放资源。
void mumble(void *arena)
{
auto_ptr<Point> ph(new Point);
smLock(arena);
// 如果这里抛出exception,心啊在就没问题了
// smUnlock(arena);
// delete p;
EH机制对于member object、base class subobjects的 constructor 影响
如果一个对象构造过程中出问题,当前的会自动处理,之前的内存要调用destructor
如果程序员写下:
// class Point3d : public Point2d { ... };
Point3d *cvs = new Point3d[512];
会发生两件事:
1.从heap中配置足以给512个Point3d objects所用的内存
2.如果成功,先是Point2d constructor,然后是Point3d constructor,会施行于每一个元素上.
如果第27个元素的Point3d constructor丢出一个exception,会怎么样呢?第27个元素, 只有Point2d destructor需要调用执行,前26个元素,Point3d destructor和Point2d destructor都需要起而执行,然后内存必须被释放回去。
对Exception Handling的支持
当一个exception发生时,编译系统必须完成以下事情:
1.检验发生 throw 操作的函数.
2.决定 throw 操作是否发生在 try 区段.
3.若是,编译系统必须把exception type拿来和每一个 catch 子句比较.
4.如果比较吻合,流程控制应该交到 catch 子句中.
5.如果 throw 的发生并不在 try 区段中,或没有一个 catch 子句吻合,那么系统必须 (a)摧毁所有active local objects,(b)从堆栈中将当前的函数"unwind"掉,(c)进行到程序堆栈中的下一个函数中,然后重复上述步骤2~5.
决定 throw 是否发生在一个 try 区段中
一个函数可以想象成好几个区域:
①try 区段以外的区域,而且没有active local objects;
②try 区段以外的区域,但有一个(以上)的active local objects需要解构;
③try 区段以内的区域。
编译器必须标示出以上各区域,并使它们对执行期的exception handling系统有所作用.一个很好的策略就是构造出 program conter-range 表格。
program counter内含下一个即将执行的程序指令,为了在一个内含 try 区段的函数中标示出某个区域,可以把program counter的起始值和结束值储存在一个表格中.
当 throw 操作发生时,当前的program counter值被拿来与对应的"范围表格"进行比较,以决定当前作用中的区域是否在一个 try 区段中。如果是,就需要找出相关的 catch 子句。如果这个exception无法被处理,当前的这个函数会从程序堆栈中被推出,而program counter会被设定为调用端地址,然后这样的循环再重新开始。
将exception的类型和每一个 catch 子句的类型做比较
对于每一个被丢出来的exception,编译器必须产生一个类型描述器,对exception的类型进行编码.如果那是一个derived type,则编码内容必须包括其所有base class 的类型信息.只编进 public base class 的类型是不够的,因为这个exception可能被一个member function捕捉,而在一个member function的范围中,在derived class 和nonpublic base class 之间可以转换.
类型描述器是必要的,因为真正的exception是在执行期被处理,其object必须有自己的类型信息。
编译器还必须为每一个 catch 子句产生一个类型描述器。执行期的exception handler会对"被丢出的object的类型描述器"和"每一个cause子句的类型描述器"进行比较,直至找到吻合的一个,或是直到堆栈已经被"unwound"而terminate()已被调用。
每一个函数会产生一个exception表格,它描述与函数相关的各区域,任何必要的善后码以及 catch 子句的位置。
当一个实际对象在程序执行时被丢出,会发生什么事
当一个exception被抛出时,exception object会被产生出来并通常放置在相同形式的exception 数据堆栈中。从 throw 端传染给 catch 子句的是exception object的地址,类型描述器(或是一个函数指针,该函数会传回与该exception type有关的类型描述器对象),以及可能会有的exception object描述器。
考虑一个 catch 子句:
catch(exPoint p)
{
// do something
throw;
}
//////////////// 没意思 //////////////

浙公网安备 33010602011771号