C++跨编译单元的静态对象初始化顺序问题及解决

引言:之前处理产品组Crash问题,发现有一些Crash是崩溃在获取xx单例代码中 ::getInstance()中,开始怀疑是单例在析构时被释放野指针了,但根据堆栈信息 判断程序基本都在刚启动阶段,析构还未触发。 所有单例也都有赋初值,也排除空指针问题。当时怀疑可能是其他线程/进程踩坑,破坏了该部分内存导致,也就没有继续深入分析,直到今天重读候老师的《Effective C++》无意间看到一章关于static初始化的深入剖析,突然有了思路。 现在这里分享一下,谨防后人踩坑。

        众所周知,C++有着十分固定的“成员初始化的次序的”,基本的基类构造先于子类构造,成员按声明次序顺序初始化等,但唯一一个例外就是在“不同编译单元内定义的non-local static对象”的初始化次序。

        所谓static对象,其寿命从被构造出来直到程序结束为止,这种对象包括global对象、定义于namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。 

        所谓编译单元是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件。(如1个cpp文件。)

        现在,我们关心的问题涉及至少两个源码文件,每一个内含至少一个 non-local static对象(非函数内的static对象)。真正的问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一个编译单元内的某个 non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义于不同编译单元内的non-local static 对象”的初始化次序并无明确定义。

  示例:

class FileSystem                   //来自你的程序
{
public:
...
   std::size_t numDisks() const;   //众多成员函数之一
...
};
extern FileSystem tfs;             //预备给客户使用的对象,全局


/===================以下为使用者代码=======================>>>///

class Directory     //由程序库客户建立
{
public:
    Directory(params){
        ...
        std::size_t disks = tfs.numDisks();   //使用tfs对象
        ...
    }
}

Directory tempDir(params);   //实例化一个Directory对象tmepDir

        现在,初始化次序的重要性显现出来了:除非 tfs 在 tempDir 之前先被初始化,否则 tempDir 的构造函数会用到尚未初始化的 tfs,造成程序crash。

        解决上述问题的方式也很简单,只需要做一个小小的变化:将每个non-local static对象搬到自己的专属函数中(该对象在此函数中声明为static)。 这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接访问这些对象。这是Singleton模式的常见实现手法。(但往往我们都写错了。)

//错误示例:
class ASingleton                              //你自己定义的一个单例
{
public:
    ASingleton* getInstance(){
        if(!s_pASingleton)
            s_pASingleton = new ASingleton;                  
        return s_pASingleton;
    }

private:
    ASingleton(){
    //...
    };

    static ASingleton* s_pASingleton;
};

ASingleton* ASingleton::s_pASingleton= nullptr;   //在cpp中初始化

       改造后:


//按上述原则优化后的代码===================》
class ASingleton                              //你自己定义的一个单例
{
public:
    ASingleton& getInstance(){
        static ASingleton s_pASingleton;  //将在类中声明改到函数中声明,并返回引用。
                                          //如果还想使用单例指针,这里也可以考虑返回指针的引用,但是看起来可能怪怪的。
        return s_pASingleton;
    }

private:
    ASingleton(){
    //...
    };
};

这个手法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问 non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。

        使用上述技术重构 tfs 和 tempDir 的例子,结果如下:

class FileSystem{...};   //同前
FileSystem& tfs()   //这个函数替换tfs对象:它在FileSystem class中可能是个Static。
{
    static FileSystem fs;
    return fs;
}

class Directory{...};      //同前
Directory::Directory(params)
{
    ...
    std::size_t disks = tfs().numDisks();   //将原来的tfs 改为 通过函数tfs()调用。
    ...
}

Directory& tempDir()  //这个函数用来替换tempDir对象:它在Directory class中可能是个static
{
    static Directory td;
    return td;               
}

最后,为避免“跨编译单元的初始化次序”问题,请以 local static 对象替换 non-local static 对象。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值