2009年12月27日星期日

全局对象的析构顺序

开宗明义:全局对象的析构顺序是不确定的,最好不要在程序逻辑中依赖这个顺序(免得出现移植问题)。

问题关注的焦点主要集中在:"cout是否会最先构造、最后析构"?

 ★关于老式编译器(03年之前推出)
  由于老式编译器在标准发布之前推出,因此对标准的实现不够好。
  老式编译器的典型代表就是VC++ 6.0。其实已经有很多人碰到了"VC6的cout提前析构"的问题。另外,如果你手头上正好有VC6,可以试试附在本文末尾的示例1的代码,看是否能够正常打印,就知道了。
  
  ★关于新式编译器(03年之后推出)
   那么新式编译器是否就完美地支持C++ 03标准呢?所以今天特地试验了手头能找到的几个C++编译器。看看它们是否能够保证cout最先构造、最后析构。另外,为了给这些新式编 译器增加点挑战,我把上述的示例1代码稍微修改了一下,成为示例2代码(说实话,这么写代码确实有点夸张),也附在本文末尾。
  手头的几种编译器测试下来,结果如下:
----------------------------
  操作系统 编译器版本      示例1  示例2
  Win2000  VC 7.1       OK   OK
  RHEL3   GCC 3.2.3      OK   OK
  Win2000  GCC 3.4.2(MingW)  OK   启动时崩溃
  Win2003  GCC 3.4.4(Cygwin)  OK   启动时崩溃
----------------------------
  对于示例2代码造成的崩溃,经过简单排查,基本可以推断是cout滞后构造导致(也就是用到cout时,它还没生出来)。而且GCC 3.4.2版本是2004年出品的,应该不算太老啊(至少03标准已经发布一年了)。从上面的结果看,Linux上版本较老的GCC反而没有问题。所以我猜测或许GCC在Windows上的移植版本有这个缺陷(仅仅是猜测啊)。

 ★总结
  根据上述的情况,建议:如果你的代码有全局对象,并且你的代码可能会跨编译器,那就避免在全局对象的构造函数和析构函数中使用标准流类库的那八个玩意儿(包括cout、cerr、clog等)。
  毕竟这八个全局对象,都有对应的标准C替代品,并不是不可替代的嘛。大伙儿犯不着冒险嘛。如果你确实舍不得流式操作符(<<和>>)或者确实看不惯printf的变参,你可以用字符串流先格式化好,再用标准C的函数输出嘛(也就多一两行代码而已嘛)。

// ========示例1代码========
#include <iostream>
using namespace std;

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

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

CFoo g_foo;

int main()
{
return 0;
}

// ========示例2代码========
class CFoo
{
public:
CFoo();
~CFoo();
};

CFoo g_foo;

#include <cstdio>
#include <iostream>
using namespace std;

CFoo::CFoo()
{
puts("puts CFoo");
cout << "cout CFoo" << endl;
}

CFoo::~CFoo()
{
cout << "cout ~CFoo" << endl;
puts("puts ~CFoo");
}

int main()
{
return 0;
}