引言:之前处理产品组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 对象。