继续整理最后一章的内容,这一篇是关于异常处理的。
注:初学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!这里throwthrow触发了软中断,此时会进行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的析构函数都需要被调用。
异常发生时编译器做了什么?
- 检验发生throw操作的函数。
- 决定throw操作是否发生在try区段中。
a. 若是,抛出的异常类型会与每一个拼配的catch子句比较,若比较吻合,控制权交给相应的catch块,否则跳转到3。
b. 若不在try区段中,跳转到3。 - 走到这一步时:
a. 销毁所有活跃的(active,即还未被析构的)局部对象。
b. 从堆栈中将当前函数展开(unwind)。
c. 进行到程序堆栈的下一个函数中去,然后重复2~3的过程。
该流程就是上面引用的描述是一致的。
最后,捕获异常时,最好使用引用:
catch (exPoint& rp)
{
//some stuff
throw;
}
这样可以避免值传递中的切割问题(rp由子类退化为父类)。
下篇会整理《深入探索C++对象模型》一书的最后一篇笔记。