先来看一个例子:
现在有两个cpp文件,分别为a.cpp, b.cpp,另外还有一个a.h文件。
a.h内容如下:
#include <string>
class A {
public:
A(std::string const& name) {register(name);}
bool register(std::string name);
}
a.cpp内容如下:
#include "a.h"
#include <map>
static std::map<std::string, int> sg_map;
bool A::register(std::string const& name) {
sg_map.insert(std::make_pair(name, 0));
return true;
}
b.cpp内容如下:
#include "A.h"
class B {
public:
static A a;
};
A B::a("B");
这里会隐藏一个潜在的bug,即sg_map.insert(std::make_pair(name, 0));这一行会崩溃,并且在进入main函数之前就会出现的崩溃。原因就是:
由于静态变量初始化顺序的不确定性导致的。在C++中,不同编译单元中的静态变量的初始化顺序是未定义的。也就是说,如果你在一个编译单元中定义了一个静态变量,并在另一个编译单元中使用它,那么你不能确定这个变量在使用时是否已经被初始化。
也就是这里在b.cpp中有一个静态变量B::a初始化时调用了另一个a.cpp中的静态变量sg_map。如果B::a的初始化过程早于sg_map,那么当调用register时,sg_map还未初始化,这时如何对sg_map进行insert操作就会产生崩溃,也就是segment fault错误。上述例子在某些情况下可能并不会崩溃,比如不同的编译器,同一个编译器的不同版本,结果可能不一样。即使不会崩溃,也不建议使用全局的静态变量。
但是如果还使用sg_map的定义放到函数register中作为局部变量使用就不会有这样的问题,原因是sg_map是一个局部静态变量,它在A::register函数中被定义。局部静态变量会在第一次进入其定义的函数或语句块时被初始化。所以,当你调用A::register函数时,可以确保sg_map已经被初始化。
a.cpp
#include "a.h"
#include <map>
bool A::register(std::string const& name) {
static std::map<std::string, int> sg_map;
sg_map.insert(std::make_pair(name, 0));
return true;
}
因此,为了避免这种问题,一种常见的做法是将全局静态变量改为局部静态变量,如上述所示,或者使用函数返回静态局部变量的方式来创建单例。例如:
std::map<std::string,int>& get_map() {
static std::map<std::string, int> s_map;
return s_map;
}
然后你可以通过调用get_map()函数来访问这个映射。
还有一种情况,如果在register中调用sg_map.empty()则不会崩溃,如下所示。这是因为std::map::empty()函数只是检查映射是否包含元素,而不需要访问映射的实际内容。即使sg_map还没有被初始化,调用empty()函数通常也不会导致崩溃。然而,当你试图在未初始化的映射中插入元素时,程序需要访问映射的内部数据结构来找到插入位置,这时如果映射还没有被初始化,就可能会导致崩溃。
a.cpp
#include "a.h"
#include <map>
static std::map<std::string, int> sg_map;
bool A::register(std::string const& name) {
bool is_empty = sg_map.empty();
sg_map.insert(std::make_pair(name, 0));
return true;
}
结论:
如果使用静态变量,并且在不同的编译单元中有不同的静态变量,最好不要使用全局静态变量,而是使用局部静态变量。这样就不会因为不同的编译单元(.cpp)中的不同的静态变量初始化顺序不确定原因导致潜在崩溃问题。
在同一个编译单元中,静态变量的初始化顺序是固定,是按照静态变量定义顺序初始化的。不同的编译单元静态变量初始化顺序不确定。
这中初始化方式同样适用于全局变量,因此对于全局变量的使用也要慎重。尤其是不同编译单元的全局变量相互调用的情况应该避免出现。