[读书笔记] 深入探索C++对象模型-第七章-站在对象模型的尖端(中)

本文详细介绍了C++异常处理机制,包括throw、catch和try块的使用。阐述了当异常发生时,编译器如何进行栈展开、析构函数调用的过程,并通过实例解析了异常处理中可能遇到的问题,如资源泄露和子对象管理。同时,建议在捕获异常时使用引用以避免值传递的切割问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继续整理最后一章的内容,这一篇是关于异常处理的。

注:初学markDown,所得尚少,排版简陋,多多包涵

欲支持异常处理,编译器的主要工作是找出catch子句,追踪程序堆栈中每一个函数的当前作用域,编译器必须提供某种查询异常对象的方法,以知道其实际类型(RTTI来源),最后,编译器还需要某种机制管理被丢出的对象,包括其产生,存储,析构,清理。

C++异常处理语法由三部分组成:
1. throw,抛出异常,其类型可以是内建类型,也可以是自定义类型。
2. 一个或者几个catch子句,每个catch子句用来表示要处理何种类型的异常,并在其紧接着的语句块中放置处理语句。
3. 一个try区段,里面的语句可能会抛出被特定类型的异常。

当一个,异常被抛出时,控制权会从当前作用域跳转出来,寻找合适的catch子句,若没有吻合的,默认的terminate()会被调用,当控制权被放弃后,堆栈中每一个函数调用会被弹出(pop up),这就是所谓的栈展开(stack unwind)。在每一个函数有脱离堆栈之前,函数的局部对象的析构函数会被调用。

此处涉及一个为什么在析构函数中不能抛出异常的问题,这是因为,如果该析构函数是由于上述的异常抛出导致的而被调用的,那么此时程序会直接terminate,因为处理异常的过程中又遇到了异常,此时无法进行合理的善后工作,关于栈展开和析构中不抛出异常,引用一段比较形象的解释:

C++程序抛了一个异常,从throw抛出到相应的catch段落被执行,之间发生了什么事?

假设是这样的call stack:
top_layer(); <== 这里有一个try_catch
  └── layer_1();
      └── layer_2();
          └── layer_3(); <== Bang!这里throw

throw触发了软中断,此时会进行stack unwind
软中断的处理流程是这样的:
layer_3() 里面有没有适当的catch? 没有。
layer_2() 里面有没有适当的catch? 没有。 
layer_1() 里面有没有适当的catch? 没有。
top_layer() 里面有没有适当的catch? (紧握双手热泪盈眶)同 志!终于找到你了。
同志,请做catch处理,我要一个大跳到你这里来了哟!
等等!layer3()中有没有局部变量需要析构一下? 有的,我先去析构他们。
好了!那layer2()中有没有局部变量需要析构一下? 有的,我去析构他们。
WTF! layer2()中有一个变态,在析构函数里又抛一个异常?
怎么办,上一个异常还没有处理好,又来一个。。。我K,我真不会了,干脆退出进程吧!
Terminal :(…


看几个实际的例子。

如下所示代码片段,左边为其行号:

001 Point*
002 mumble()
003 {
004     Point *pt2, *pt2;
005     pt1 = foo();
006     if(!pt1)
007         return 0;
008     
009     Point p;
010
011     pt2 = foo();
012     if(!pt2)
013         return pt1;
014
015     ...
016 }
  • 如果在第5行调用foo()时,抛出了一个异常,此处并没有try catch语句,也没有局部变量需要析构
  • 但是若在第11行调用foo()时,抛出异常,异常处理机制必须在从程序堆栈中展开这个函数之前,先调用第九行局部变量,p的析构函数。

另外,有很多表面看似没有问题的代码,在有异常被抛出时,就可能会产生问题,例如:

void mumble(void* arena)
{
    A*  = new A;
    lock(arena);
    //此处若抛出异常,会有问题。
    //...
    unlock(arena);
    delete p;
}

如注释中所示,释放资源之前有异常抛出时,会导致资源泄露。
解决方法有多种.
一种是添加catch处理,在catch中释放资源:

void mumble(void* arena)
{
    A*  = new A;
    try
    {
        lock(arena);
        //...
    }
    catch(...)
    {
        unlock(arena);
        delete p;
        throw;
    }
    unlock(arena);
    delete p;
}

另外,可以使用只能指针来管理对象,这样一旦有异常发生,智能指针的析构函数会被调用,其所容纳的资源也会一起释放掉。

异常处理中,对于含有子对象(子类)的类,处理要更为复杂,异常抛出时,一个类对象只被部分构造,那么也只能析构这部分构造好的对象,例如,X中含有A,B,C三个子对象,它们都有各自的构造函数和析构函数,正常的构造顺序为A->B->C->X:

  • 若A构造函数抛出异常,则没有析构函数需要被调用。
  • 若B构造函数熬出异常,则A的析构函数必须被调用,但C不用。

再比如以下例子:

//class A : public B { ... };
A* pArrA = new A[128];

若在构造第33个A时,抛出一个异常,那么:

  • 对于第33个元素,只有B的析构函数需要被调用。
  • 而前22个元素,B和A的析构函数都需要被调用。
异常发生时编译器做了什么?
  1. 检验发生throw操作的函数。
  2. 决定throw操作是否发生在try区段中。
    a. 若是,抛出的异常类型会与每一个拼配的catch子句比较,若比较吻合,控制权交给相应的catch块,否则跳转到3。
    b. 若不在try区段中,跳转到3。
  3. 走到这一步时:
    a. 销毁所有活跃的(active,即还未被析构的)局部对象。
    b. 从堆栈中将当前函数展开(unwind)。
    c. 进行到程序堆栈的下一个函数中去,然后重复2~3的过程。

该流程就是上面引用的描述是一致的。

最后,捕获异常时,最好使用引用:

catch (exPoint& rp)
{
    //some stuff
    throw;
}

这样可以避免值传递中的切割问题(rp由子类退化为父类)。

下篇会整理《深入探索C++对象模型》一书的最后一篇笔记。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值