7.2 异常处理

Q1:编译器的任务

• 支持异常处理,编译器的主要工作是找到 catch 子句,以处理被抛出的异常。需要支持以下几个工作:

• 追踪程序堆栈中每一个函数的目前作用区域(包括追踪函数中局部类对象当时的情况)

• 提供某种查询异常对象的方法,以知道其实际类型

• 管理被抛出对象的机制,包括其产生,存储以及析构



Q2:异常处理知识点回顾

• 当异常被抛出时,控制权将从函数调用中释放出来,并寻找一个吻合的 catch 子句。如果没有吻合的,则默认的 terminate() 会被调用。放弃控制权后,堆栈中的每一个函数调用也就被推离,推离前,函数的局部类对象的析构函数会被调用

• 异常在不同的位置抛出,将程序分为了不同的区域:

Eg:

        Point * mumble()
        {
            Point * pt1, * pt2;
            pt1 = foo();         //loc1
            if(!pt1)
                return 0;

            Point p;

            pt2 = foo();          //loc2
            if(!pt2)
                return pt1;

            …
        }

• 如果一个异常在第一次调用 foo() 时被抛出,则这个 mumble() 函数会被推出程序堆栈,由于此时的 foo()不在一个try 区段中,因此不需要尝试与 catch 子句吻合,也没有任何类对象需要析构。

• 如果一个异常在第二次调用foo() 时被抛出,则就必须在将这个函数从堆栈中推出前,先调用 P 的析构函数

因此,上述函数在异常处理下被分为两个语意(执行期语意)不同的区域。因此,编译器的做法有以下两种:

        1. 把两块区域以个别的“将被摧毁的局部对象”链表联合起来,该链表在编译器设置妥当

        2. 让两块区域共享同一个链表,该链表会在执行期放大或缩小


• 异常处理改变了函数在资源管理上的语意:

Eg:

        void mumble(void * area)
        {
            Point * p = new Point;
            smLock(area);              //上锁

            smUnLock(area);        //解锁
            delete p;
        }

• 异常处理机制此时将整个函数视为一个单一区域,不要担心将函数从程序堆栈中推出

• 从语意上而言,函数被推出之前,需要解锁共享内存,并delete p。支持异常处理的编译器为程序添加默认catch 子句,将程序转为如下:

        void mumble(void * area)
        {
            Point * p;
            p = new Point;

            try
            {
                smLock(area);
                //…
            }
            catch(…)
            {
                smUnLock(area);
                delete p;
                throw;    //解锁并释放 p 后,继续抛出原异常
            }

            smUnLock(area);
            delete p;
        }

将函数分为了两个区域:

        1. try块以外的区域,异常机制除了“pop”程序堆栈外,不做任何事情

        2. try块以内的区域(以及默认 catch 子句)

• 一个建议:将资源需求封装于一个类对象体内,并由析构函数来释放资源

• 支持异常处理,会使拥有类成员子对象或基类子对象的类的构造更复杂。一个类如果被部分构造,其析构函数必须只施行于哪些已经被构造的子对象或成员对象



Q3:对异常处理的支持

• 当异常发生时,编译系统必须完成的事情:

    1. 检验发生 throw 操作的函数

    2. 决定 throw 操作是否发生在 try 区段

    3. 若是,把异常类型拿出来与每一个 catch 子句进行比较

    4. 如果比较后吻合,将流程控制交到 catch

    5. 如果 throw 的发生不在 try 区段,或者没有一个 catch 与其吻合,则系统必须:

        i. 摧毁局部对象

        ii. 从堆栈中将当前函数推出

        iii. 进入程序堆栈中的下一个函数中,重复步骤 2~5

• 决定 throw 是否发生在一个 try 区段

• 编译器需要将一个函数分为以下几个区域:

        1. try 区段以外的区域,且没有 active 局部对象
        2. try 区段以外的区域,但有一个以上的 active 对象需要析构
        3. try 区段以内的区域

• 编译器需要标识这些区域,方法:构造出 program counter-range(程序计数器范围)表格。将try的位置在计数器中的起始位置与try区段的范围存储在该表格中。当 throw 操作发生时,比较目前程序计数器的值与表格中的值,即可判断目前作用域是否在 try 区段内

• 将异常的类型和每一个 catch 子句的类型作比较

• 对每一个抛出的异常,编译器产生一个类型描述器,对异常的类型进行编码。

• 编译器为每一个 catch 子句产生一个类型描述器

• 将抛出的异常的类型描述器与 catch 子句的类型描述器进行比较,直到找到吻合的或者直到堆栈已经被推出为空,调用 terminate

• 当一个实际对象在程序执行时被抛出,会发生什么事情?

• 注:只有在一个 catch 子句评估完毕且知道它不会再抛出异常之后,真正的异常对象才会被销毁

Eg:

            exVertex errVer;

            //…
            mumble()
            {
                //…
                if(mumble_cond)
                {
                    throw errVer;         //抛出全局对象
                }
            }

此时抛出的对象是全局对象的一个复制品,而全局对象没有被繁殖,意味着:在 catch 子句中对于异常对象的任何改变都是局部的,不会影响全局对象

• catch 局部对象与引用的区别:

Eg: 假设此时抛出的异常对象是 exVertex 类型

            //class exVertex : public exPoint{//…}

            catch(exPoint p)
            {
                //do something
                throw;
            }

此时,由于 p 是一个对象而不是引用,因此,其内容被拷贝构造的时候,将会截断异常对象的 non-exPoint 部分;当这个异常再一次抛出时,p 是局部对象,会在 catch 子句的末端被销毁,抛出 p 需要产生另一个临时对象,该对象将丧失原来的异常对象的 exVertex 部分,因此,将再次抛出原来的异常对象,该子句中对 p 的修改将被抛弃

Eg:

            catch(exPoint &p)
            {
                //do something
                throw;
            }

此时,由于 p 是一个引用,因此任何对此对象的改变都会被繁殖到下一个 catch 子句

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值