C++对象模型——异常处理 (Exception Handling)(第七章)

7.2    异常处理 (Exception Handling)

    欲支持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来释放资源.
    如果程序员写下:
// 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描述器.

你可能感兴趣的:(深度探索C++对象模型,C++)