首页 > 为什么c++抛出异常后还能对函数内的局部对象进行析构?

为什么c++抛出异常后还能对函数内的局部对象进行析构?

C++是如何确保出了异常还能调用析构函数的


在抛出异常后调用栈内存对象的析构函数,在C++标准里有规定。析构函数本来就不是显式调用的,编译器和运行环境自然知道什么时候应该调用析构函数,只要它们是按照C++标准实现的。至于如何实现,我想也不难吧,只需要给初始化过的对象做个标记,处理异常的时候逐个调用它们的析构函数不就好了。


如何确保?标准确保。因为这是标准规定的

以下摘自 C++ 11 Standard (draft N3690)

15.2 Constructors and destructors [except.ctor]

不光保证了被析构,还规定了析构的顺序。

1) As control passes from the point where an exception is thrown to a handler, destructors are invoked for all automatic objects constructed since the try block was entered. The automatic objects are destroyed in the reverse order of the completion of their construction.

比较特殊一点的,如果异常发生在构造或析构的时候,其子对象也能确保被正确的析构。而该对象本身呢?构造的时候它还不存在呢,所以无须担心。析构的情况会在后面说明。

2) An object of any storage duration whose initialization or destruction is terminated by an exception will have destructors executed for all of its fully constructed subobjects (excluding the variant members of a union-like class), that is, for subobjects for which the principal constructor (12.6.2) has completed execution and the destructor has not yet begun execution. Similarly, if the non-delegating constructor for an object has completed execution and a delegating constructor for that object exits with an exception, the object’s destructor will be invoked. If the object was allocated in a new-expression, the matching deallocation function (3.7.4.2, 5.3.4, 12.5), if any, is called to free the storage occupied by the object.

这整个过程被称为 "stack unwinding",翻译过来叫: 堆栈辗转开解

3) The process of calling destructors for automatic objects constructed on the path from a try block to the point where an exception is thrown is called “stack unwinding.” If a destructor called during stack unwinding exits with an exception, std::terminate is called (15.5.1). [Note: So destructors should generally catch exceptions and not let them propagate out of the destructor. —end note]

注意看那个 “Note”,标准对编译器做了要求,对于析构函数来说,是要对自身负责的。

正是因为 stack unwinding 的保证作为基础,才有了我们所熟知的 RAII 技术。

另外可以注意到,如果在 stack unwinding 期间抛出异常呢?就只能调用 std::terminate 了:

15.5.1 The std::terminate() function [except.terminate]

2) In such cases, std::terminate() is called (18.8.3). In the situation where no matching handler is found, it is implementation-defined whether or not the stack is unwound before std::terminate() is called. In the situation where the search for a handler (15.3) encounters the outermost block of a function with a noexcept-specification that does not allow the exception (15.4), it is implementation-defined whether the stack is unwound, unwound partially, or not unwound at all before std::terminate() is called. In all other situations, the stack shall not be unwound before std::terminate() is called. An implementation is not permitted to finish stack unwinding prematurely based on a determination that the unwind process will eventually cause a call to std::terminate()

stack unwinding 的过程并不保证做完,但最终肯定是要调用 std::terminate 来终止。


引用维基百科的描述,讲的比我们解释的清楚,黑体是我加的:

throw

throw是一个C++关键字,与其后的操作数构成了throw语句,语法上类似于return语句。throw语句必须被包含在try块之中;可以是被包含在调用栈的外层函数的try中。

执行throw语句时,其操作数的结果作为对象被复制构造为一个新的对象,放在内存的特殊位置(既不是堆也不是栈,Windows上是放在“线程信息块TIB”中)。这个新的对象由本级的try所对应的catch语句逐个做类型匹配;如果匹配不成功,则与本函数的外层catch语句依次做类型匹配;如果在本函数内不能与catch语句匹配成功,则递归回退到调用栈的上一层函数内从函数调用点开始继续与catch语句匹配。重复这一过程直到与某个catch语句匹配成功或者直到主函数main()都不能处理该异常。

因此,throw语句抛出的异常对象不同于一般的局部对象。一般的局部对象会在其作用域结束时被析构。而throw语句抛出的异常对象驻留在所有可能被激活的catch语句都能访问到的内存空间中。

throw语句抛出的异常对象在匹配成功的catch语句的结束处被析构(即使该catch语句使用的是非“引用”的传值参数类型)。

由于throw语句都进行了一次副本拷贝,因此异常对象应该是可以copy构造的。但对于Microsoft Visual C++编译器,异常对象的复制构造函数即使私有的情形,异常对象仍然可以被throw语句正常抛出;但在catch语句的参数是传值时,在catch语句处编译报错:

cannot be caught as the destructor and/or copy constructor are inaccessible”。

抛出一个表达式时,被抛出对象的静态编译时类型将决定异常对象的类型。

栈展开

栈展开(unwinding)是指当前的try...catch...块匹配成功或者匹配不成功异常对象后,从try块内异常对象的抛出位置,到try块的开始处的所有已经执行了各自构造函数的局部变量,按照构造生成顺序的逆序,依次被析构。如果当前函数内对抛出的异常对象匹配不成功,则从最外层的try语句到当前函数体的起始位置处的局部变量也依次被逆序析构,实现栈展开,然后再回退到调用栈的上一层函数内从函数调用点开始继续处理该异常。

catch语句如果匹配异常对象成功,在完成了对catch语句的参数的初始化(对传值参数完成了参数对象的copy构造)之后,对同层级的try块执行栈展开。

由于线程执行时,被调用的函数的参数、返回地址、局部变量等都是依函数调用次序保存在函数调用栈(即线程运行时栈)上。当前被调用函数的参数、局部变量名字可以覆盖掉早前调用函数的同名变量,看起来就是只有当前函数内的名字可以访问,早前调用的函数内部的名字都不可访问,就像磁带被“卷起”。异常处理时按照函数调用顺序的逆序析构,依次析构各个被调函数的局部变量,就类似把已经卷起的“磁带”再展开,抹去上面记录的数据,故此“栈展开”得名。

【热门文章】
【热门文章】