进程之死
对于一个用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之。