代码改变世界

RAII、栈展开和程序终止

2011-07-06 00:11  zhenjing  阅读(3168)  评论(0编辑  收藏  举报

缘起

在项目中发现某些情况下,对象的析构函数不被调用,比如程序调用exit(), 异常终止等。那么,析构函数什么情况下不会被调用呢?

RAII

RAII(资源获取即初始化RAII, Resource Acquisition Is Initialization)是C++编程中很重要的一项技术。其原理是在对象析构函数中释放该对象获取的资源,利用栈展开过程栈上对象的析构函数将被自动调用的保证,从而正确地释放先前获取的资源。RAII只有在栈展开正常执行的前提下才能正常工作。函数调用和正常的C++异常处理流程(异常处于try-catch块)都存在栈展开。

栈展开

最常见的栈展开就是正常的函数调用,任何一个函数返回都存在栈展开。C++引入异常机制后,当程序抛出异常,在异常向上传递的过程中,其函数调用栈也会展开。

程序终止

先摘录一段来自stackoverflow的回答:

The Standard defines three ways to end execution of a C++ program:

  • Return from main. Objects with automatic storage (function-local) have already been destroyed. Objects with static storage (global, class-static, function-static) will be destroyed.
  • std::exit from <cstdlib>. Objects with automatic storage are NOT destroyed. Objects with static storage will be destroyed.
  • std::abort from <cstdlib>. Objects with automatic and static storage are NOT destroyed.

Also relevant is std::terminate from <exception>. The behavior of terminate can be replaced using std::set_terminate, but terminate must always "terminate execution of the program" by calling abort or some similar implementation-specific alternative. The default is just { std::abort(); }.

C++ will call std::terminate whenever an exception is thrown and C++ can't reasonably do stack unwinding. For example, an exception from a destructor called by stack unwinding or an exception from a static storage object constructor or destructor. In these cases, there is no (more) stack unwinding done.

C++ will also call std::terminate when a matching catch handler is not found. In this single case, C++ may optionally unwind to main before calling terminate. So your example might have different results with a different compiler.(implementation-defined)

So if you use RAII correctly, the remaining steps to "leak-proof" your program are:

  • Avoid std::abort.
  • Either avoid std::exit or avoid all objects with static storage duration.
  • Put a catch (...) handler in main, and make sure no allocations or exceptions happen in or after it.
  • Avoid the other programming errors that can cause std::terminate.
    • (On some implementations, functions compiled with a C compiler act like they have C++'s empty throw() specification, meaning that exceptions cannot be thrown "past" them even though they have no destructors to be called.)

简单示例:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdexcept>   // for build-in exception, such as runtime_error
#include <exception>   // c++ header, must use std:: to call function, such as terminate()

class Test
{
    public:
    ~Test() {
        printf("======== Call ~Test ========\n");
    }
};

static Test a;

int main(int argc, char** argv)
{
    Test t;

    // exit(-1);   // Objects with automatic storage are NOT destroyed. Objects with static storage will be destro
yed.
    // _exit(-1);  // Objects with automatic and static storage are NOT destroyed.
    // abort();    // Objects with automatic and static storage are NOT destroyed.
    // std::terminate(); // Objects with automatic and static storage are NOT destroyed.

    // sleep(10);  // signal exit(CTRL+C). // Objects with automatic and static storage are NOT destroyed.

    throw std::runtime_error("test error");  // Like std::terminate()
    return 0;  // Normal. Objects with automatic and static storage are destroyed.
}

简单讲就是:除了从main函数返回之外,调用exit(), abort(), terminate()都不保证调用栈正常展开,即RAII将失效。话说回来,程序都终止了,栈不展开关系也不大,RAII失效就失效吧。但是,RAII失效意味着RAII并不能保证资源一定被释放。对于进程生存期的资源,如文件描述符(打开文件,socket等)、内存等,即使RAII失效,进程占用的资源也终将得到释放,但对于内核生存期和文件系统生存期的资源,如IPC(信号量、消息队列、共享内存)、文件等,RAII是存在缺陷的。是否有更加可靠的办法来释放系统资源呢?这个需要进一步探索。

其实,除了上述4种程序的终止方式外,还有下面几种异常的程序终止方式:

1)    系统调用_exit();

2)    非法内存访问;

3)    信号终止;

4)    内存耗尽;

5)    (欢迎补充)

总之,当程序异常终止,堆栈是无需展开的,其栈上对象也将不被显示析构。

参考:

RAII and Stack unwinding

相关文章:

[Advance] How to debug a program (上)