这两天项目代码中遇到一个很疑惑的问题,问题可以描述为:一个静态成员初始化的时候直接core掉,该静态成员初始化时通过另外一个文件中静态成员来完成。该问题同样发生在全局对象上。该问题可以描述为今天要讨论的:变量的静态初始化顺序。
具体可以用代码简述如下:
//test1.cpp
#include <string>
std::string a = "test";
//test2.cpp
#include <iostream>
extern std::string a;
std::string b = a;
int main()
{
std::cout<<b<<std::endl;
}
当执行如下编译命令:
g++ -g test1.cpp test2.cpp
执行结果正确,输出”test”文本,但当执行如下编译指令:
g++ -g test2.cpp test1.cpp
执行结果如下:
Segmentation fault (core dumped)
调试core文件,函数帧栈如下:
(gdb) bt
#0 0x00007ff5f0932f2b in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string
(std::string const&)() from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#1 0x0000000000400af8 in __static_initialization_and_destruction_0 (__initialize_p=1,
__priority=65535) at test4.cpp:7
#2 0x0000000000400b24 in _GLOBAL__sub_I_b () at test4.cpp:12
#3 0x0000000000400c2d in __libc_csu_init ()
#4 0x00007ff5f02de700 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6
#5 0x00000000004009c9 in _start ()
可以看到程序在静态初始化全局对象时,调用string的copy constructor导致内存访问异常。该问题的原因就是依赖的静态成员还没有进行初始化导致的。
我们知道对于程序的所有全局和静态数据成员,都是放在全局数据区。对于已经初始化的全局和静态变量时存放在可执行文件的数据段(.data),而对于未初始化的全局和静态变量,则在BSS段中(BSS段在生成的可执行文件中并不存在,直到程序被加载到内存中),程序被加载到内存后,BSS段的内存被清零。
这里首先强调一个概念:静态初始化:静态对象(包括全局和静态变量)的初始化。
在程序加载到内存后,对于存在数据段中的全局和静态变量,dynamic linker loader在程序员指定的动态初始化发生前,会保证每一个静态对象初始化为零(当然,这里只针对内置数据类型)。而对于自定义数据类型,例如程序中的string对象,如果a未先被动态初始化,那么a的内存空间的数据就是未定义,就会出现调用string的copy constructor导致内存访问异常。这种情况很容易在跨文件引用时出现。
在《C++编程思想》p245中针对静态初始化依赖问题给出了一下几点建议:
- 避免静态对象初始化的依赖;
- 把静态对象放到同一个编译单元中,即同一文件。
- 如果一定要把静态对象放到不同的编译单元中,可使用两种程序设计技术进行解决
这里只说一下里面提到的技术二:通过函数获取静态对象。
其实我们始终关心的对象的初始化顺序,而不是初始化时间。为了解决这个问题,这种技术采用:把一个静态对象放到一个返回该对象引用的的函数中,访问该静态对象的唯一途径就是通过该函数,而在函数第一次被调用时,就会强迫该静态对象进行初始化。该技术依赖的特性是:函数内部的静态对象在函数第一次被调用时进行初始化,且在程序生命周期只被初始化一次。这样静态对象的初始化顺序就是由设计的代码而不是链接器的链接顺序来决定。上述的代码通过该技术,可以更改为为下面:
//test1.cpp
#include <string>
using namespace std;
const string & GetA()
{
static string a = "test";
return a;
}
//test2.cpp
#include <string>
#include <iostream>
using namespace std;
const string &GetA();
string b = GetA();
int main()
{
cout<<b<<endl;
}
上面的代码就不会存在一开始出现的那种问题。