4 评论

C++ 对象是怎么死的?进程篇

  我承认这个帖子的名称有标题党的嫌疑,但是暂时想不出更好的名称了,只好先这样了 :-(
  由于前天的帖子聊了架构设计的多进程问题,所以今天想起来要聊一下与“C++进程终止”相关的那些事。与前几个 C++ 帖子的风格类似,今天聊的内容,尽量局限于标准 C++ 范畴,尽量不涉及特定的操作系统平台。

★关于进程的三种死法


  由于今天讲的是“进程篇”,自然得先搞明白进程的几种死法。其实进程和大活人一样,也有三种死法,分别是“自然死亡、自杀、它杀”。这三种死亡方式具体如下:

◇自然死亡


  望文生义,自然死亡就是最自然的进程退出方法。具体表现为通过return语句结束main函数。由于这种方法最优雅(后面会说),如果没有其它特殊原因,强烈建议采用这种死法。

◇自杀


  所谓的自杀,就是进程自己调用某些 API 来自行了断。在标准 C++ 中,这几个函数(exit、abort、terminate、unexpected)可以用于进程自杀。如果没有额外设置,unexpected 函数默认会调用 terminate 函数,terminate 函数默认会调用 abort 函数。所以自杀的方式基本上也就是 exit 和 abort 两种。exit 相对 abort 来说温和一些,所以下文称 exit 为温和自杀;相对地,把 abort 称为激进自杀

◇它杀


  它杀其实也挺好理解,就是当前进程被其它进程杀死。标准 C++ 没有提供用于它杀的 API 函数,因此常用的方法是通过某些跨平台的库(如 ACE)提供的 API 函数或者调用某些外部命令(如 Posix 系统的 kill 命令)来实现。
  上面说了这几种死法,有同学要问了:进程不同的死法和 C++ 对象有什么关系捏?其实关系大大滴,请听我细细道来。

★类对象的析构(销毁)


  首先把类对象分为三种:局部非静态对象、局部静态对象、非局部对象(出于习惯,以下简称全局对象)。对于尚不清楚这几种对象差异的同学,请先找本 C++ 入门书拜读一下。
  进程不同的死法对于这几种对象是否能销毁会有很大的影响。请看如下的对照表:
---------------------------------
|     | 局部非静态对象 | 局部静态对象 | 全局对象 |
|自然死亡 | 能       | 能      | 能    |
|温和自杀 | 不能      | 能      | 能    |
|激进自杀 | 不能      | 不能     | 不能   |
|它杀   | 不能      | 不能     | 不能   |
---------------------------------
  从这个对照表可以看出,激进自杀和它杀的效果类似(各种类对象都【无法】正常销毁)。所以我们在写程序时要极力避免上述这两种情况。
  另外,温和自杀也有不爽之处:不能正确地销毁局部非静态对象。准确地说,应该是:在调用exit之前已经构造但是尚未析构的局部非静态对象将再也不会被析构。所以温和自杀也要避免使用。
  综上所述,最正经、最靠谱的死法就是第一种:自然死亡。

★析构的顺序


  那么,是不是只要让进程自然死亡就万事大吉了?非也!即使所有的类对象都会被析构,还有另一个棘手的问题:析构的顺序。
  先来看下面一个例子:

class CFoo
{
public:
    CFoo()
    {
        cout << "CFoo" << endl;
    }

    virtual ~CFoo()
    {
        cout << "~CFoo" << endl;
    }
};

  上述示例挺简单的(有效代码仅6行),大伙儿能看出有什么问题吗?(如果你一眼就看出问题之所在,恭喜你——你的 C++ 水平够高,后面的内容你不用看了)
  对于用户定义的全局对象,在 C++ 标准中并【没有】规定它们构造和析构的先后顺序;对于诸如标准输入输出流的 cout、cerr 等全局对象,在 C++ 03 标准中(27.4.2.1.6章节)有提及如何保证它们在最后析构。但由于某些老式编译器并未完全遵照标准实现,导致标准输入输出流的几个全局对象【有可能】被提前析构。
  基于上述原因,假如 CFoo 类也定义了一个全局对象 g_foo。当 g_foo 析构的时候,cout 对象【有可能】已经先死了(取决于具体的环境,详见《关于标准输入输出流的进一步探讨》)。在这种情况下,CFoo 析构函数的打印语句由于引用了已死的对象,可能会导致【不可预料的后果】(比如进程崩溃)。
  从上面的例子可以看出,如果你在程序中使用了全局对象或者静态对象,那你要非常小心地编写相关 class/struct 的析构函数代码,尽量【不要】在它们的析构函数中引用其它的全局对象或静态对象。当然啦,假如能避免使用全局对象和静态对象,就更好了。
  另外,在 C++ 经典名著《Modern C++ Design》的第6章详细描述了关于单键/单例(Singleton)销毁的一些细节、场景及解决方法。大伙儿可以去拜读一下。
  下一个帖子,咱们来聊一下和线程有关的C++对象是怎么死的
版权声明
本博客所有的原创文章,作者皆保留版权。转载必须包含本声明,保持本文完整,并以超链接形式注明作者编程随想和本文原始地址:
https://program-think.blogspot.com/2009/02/cxx-object-destroy-with-process.html?m=0

4 条评论

  1. 自然死亡、自杀、它杀。形象,赞 :D

    回复删除
  2. 你的语言太暴力了,建议将标题改为"C++对象的退出方式", "死"字太难听,建议将其改为"退出","结束"等,例如进程的三种结束方式,应分别称为"自然退出","自主强制退出","外部强制退出"或"远程强制退出"
    另外我老早就是你博客的读者了,以前给你回复的时候总有好几十的网友比我捷足先登,我从来没有排名这么靠前过,今天有幸抢了回第二名的位置,心情无比激动.

    回复删除
    回复
    1. TO 林秋輝
      多谢提建议 :)

      这个系列《C++对象是怎么死的》,当初俺是刻意用了这个标题,比较形象。
      而且可以配合“自杀、它杀、自然死亡”等词汇 :)
      这样一来,读者的印象比较深刻,不容易忘记。

      另,
      俺早期的博文,评论数比较少(那时候读者还不多)。
      如今动不动就超过 200 条,然后就得点击“加载更多”

      删除
    2. 本文对于其他类C++语言(比如PHP)也有一定借鉴意义。

      关于“死”这个字,我赞同博主的做法,就像英文一样生动得多,
      中文里面很多十分晦涩的IT行业专业术语,在翻译之前,其实都是简单、日常生活中多用的英语词汇。

      删除