进程之死

  对于一个用C++写的程序,被加载至内存后运行,最终走向死亡。程序的死亡大致有三种:

  • 自然死亡,即无疾而终,通常就是main()中的一个return 0;
  • 自杀,当程序发现自己再活下去已经没有任何意义时,通常会选择自杀。当然,这种自杀也是一种请求式的自杀,即请求OS将自己毙掉。有两种方式:void exit(int status)和void abort(void)。
  • 他杀,同现实不同的是,程序家族中的他杀行径往往是由自己至亲完成的,通常这个至亲就是他的生身父亲(还是母亲?)。C++并没有提供他杀的凶器,这些凶器往往是由OS直接或者间接(通过一些进程库,如pthread)提供的。

  自然死是最完美的结局,他杀是我们最不愿意看到的,自杀虽是迫不得已,但主动权毕竟还是由程序自己掌控的。下面探究程序一下不同的死亡方式对对象的析构有何影响。

程序死亡方式对对象析构的影响

  C++程序中大致有三种对象:全局对象、局部静态对象、局部非静态对象(自动对象)。举例说明之:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;
 
struct Foo
{
    Foo(){ cout<<"Foo"<<endl; }
    ~Foo(){ cout<<"~Foo"<<endl; }
    /* some other sources here */
};
Foo Global;
void quit();
int
main()
{
    static Foo StaticLocal;
    Foo Local;
    //~ quit();
    //~ abort();
    return 0;
}
void quit()
{
    Foo AnotherLocal;
    exit(1);
}

  编译运行这个程序,程序将正常退出。运行过程中,Global对象在进入main之前首先被构造,其次是StaticLocal和Local。main函数退出之前,Local和StaticLocal被析构,main退出后Global也将被析构。
  如果将17行处quit()的注释去掉,我们将会看到4个对象被构造,但却只有两个对象被析构,分别是Global和StaticLocal对象,其他两个对象Local和AnotherLocal对象的析构函数将不会被调用。
  如果将18行处abort()的注释去掉(quit()被注释),3个对象对象被构造,但在程序退出之前没有任何一个对象的析构函数被调用。
  也就是说,正常情况下,所有类型的对象都将被析构;由exit退出时只有非自动对象被析构;abort被调用时,程序将直接退出,任何对象的析构函数都不会调用。下面着重介绍下exit的行为。

exit做了什么

  介绍exit之前,不得不提void atexit(void (*f)(void) )函数。atexit,顾名思义,它描述了exit里面要做些什么。可以看出,它接受一个void f(void)形式的函数的指针。使用atexit我们可以向exit注册一些列的函数,这些函数在exit中被调用,调用的顺序与它们被注册的顺序相反。你可以使用下面的代码来验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;
 
void f1(){ cout<<"f1"<<endl; }
void f2(){ cout<<"f2"<<endl; }
int
main()
{
    atexit(f1);
    atexit(f2);
    exit(1);
    return 0;
}

  void exit(int status)被调用时,它首先调用全局的或者静态的对象的析构函数,然后调用atexit所注册的函数。如果这些函数中的某一个再次调用exit,your nightmare is coming。最后,exit会将代表程序执行状态的status“返回”(确切的说应该叫做传递,因为exit永远不会返回调用方)给当前程序的父进程。
  值得一提的是,执行exit结束程序,虽然自动对象的析构函数不被调用,但当程序结束时,OS会将该程序占用的资源全部释放。这些资源包括该程序申请的内存(堆)、打开的文件句柄、管道(Unix/Linux)、socket等。这样一来,似乎那些析构函数不被调用并不会有什么问题。确实,但可惜这只适用于单线程的程序。对于多线程程序来说,只有当整个进程结束时,它占用的资源才会被OS释放,这时某个线程的exit就可能带来麻烦(比如内存泄露)。怎么办呢?

使用异常

  使用C++提供的异常机制,可以很好的解决上面提出的问题。我们可以在需要exit的地方抛出(throw)异常,然后在捕获(catch)异常处调用exit,这样,所有需要的析构函数都将被调用。代码通常是这个样子滴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <cstdlib>
using namespace std;
 
struct Foo
{
    Foo(){ cout<<"Foo"<<endl; }
    ~Foo(){ cout<<"~Foo"<<endl; }
    /* some other sources here */
};
struct except: public exception
{
    const char* what() const throw()
    {
        return "except";
    }
};
 
Foo Global;
void quit();
int
main()
{
    try
    {
        static Foo StaticLocal;
        Foo Local;
        quit();
    }
    catch(const exception& e)
    {
        cerr<<e.what()<<endl;
        exit(0);
    }
    return 0;
}
void quit()
{
    Foo AnotherLocal;
    //~ exit(1);
    throw except();
}

输出:

Foo
Foo
Foo
Foo
~Foo
~Foo
except
~Foo
~Foo

补充:还有一个与atexit()相似的函数叫on_exit(),Google之。

Tags: ,.
你好!除了代码,此处没有多少原创之物,皆为本人搜集、整理、总结之记录与心得,欢迎转载分享!转载时请尽量注明出处,将不胜感激。祝你健康、快乐!
Home

Be the first to comment on this entry.

Name(required)
Mail (required),(will not be published)

RFC: Request For Comments. Orz..

Website(recommended)