C++ 全局变量初始化学习笔记

根据 C++ 标准,全局变量的初始化要在 main 函数执行前完成,常识无疑,但是这个说法有点含糊,main 函数执行前到底具体是什么时候呢?是编译时还是运行时?答案是既有编译时,也可能会有运行时(seriously), 从语言的层面来说,全局变量的初始化可以认为分成以下两个阶段(c++11 N3690 3.6.2):

static initialization: 静态初始化指的是用常量来对变量进行初始化,主要包括 zero initialization 和 const initialization,静态初始化在程序加载的过程中完成,对简单类型来说,从具体实现上看,zero initialization 的变量会被保存在 bss 段,const initialization 的变量则放在 data 段内,程序加载即可完成初始化,这和 C 语言里的全局变量初始化基本是一致的。

dynamic initialization:动态初始化主要是指需要经过函数调用才能完成的初始化,比如说:int a = foo(),或者是复杂类型(类)的初始化(需要调用构造函数)等。这些变量的初始化会在 main 函数执行前由运行时调用相应的代码从而得以进行。

需要明确的是:静态初始化执行先于动态初始化!只有当所有静态初始化执行完毕,动态初始化才会执行。显然,这样的设计是很直观的,能静态初始化的变量,它的初始值都是在编译时就能确定,因此可以直接 hard code 到生成的代码里,而动态初始化需要在运行时执行相应的动作才能进行,因此,静态初始化先于动态初始化是必然的。

初始化的顺序

对于出现在同一个编译单元内的全局变量来说,它们初始化的顺序与他们声明的顺序是一致的(销毁的顺序则反过来),而对于不同编译单元间的全局变量,c++ 标准并没有规定它们之间的初始化(销毁)顺序应该怎样,因此实现上完全由编译器自己决定,一个比较普遍的认识是:不同编译单元间的全局变量的初始化顺序是不固定的,哪怕对同一个编译器,同一份代码来说,任意两次编译的结果都有可能不一样[1]。

因此,一个很自然的问题就是,如果不同编译单元间的全局变量相互引用了怎么办?

当然,最好的解决方法是尽可能的避免这种情况(防治胜于治疗嘛),因为一般来说,如果出现了全局变量引用全局变量的窘况,那多半是程序本身的设计出了问题,此时最应该做的是回头重新思考修改程序的结构,而不是急着穷尽技巧来给错误的设计打补丁。

---- 说得轻松。

几个技巧

好吧,我承认总有那么一些特殊的情况,是需要我们来处理这种在全局变量的初始化函数里竟然了引用别的地方的全局变量的情况,比如说在全局变量的初始化函数里调用了 cout, cerr 等(假设是用来打 log, 注意 cout 是标准库里定义的一个全局变量)[2],那么标准库是怎样保证 cout 在被使用前就被初始化了呢? 有如下几个技巧可以介绍一下。

Construct On First Use

该做法是把对全局变量的引用改为函数调用,然后把全局变量改为函数内的静态变量:

int get_global_x() { static X x; return x.Value(); }

这个方法可以解决全局变量未初始化就被引用的问题,但还有另一个对称的问题它却没法解决,函数内的静态变量也属于 variables with static storage, 它们的析构顺序在不同的编译单元间也是不确定的,因此上面的方法虽然能保证 x 初始化了才被使用,但却没法保证不出现,如果 x 析构了 get_global_x() 还被调用的这种情况。

一个改进的做法是把静态变量改为如下的静态指针:

int get_global_x() { static X* x = new X; return x->Value(); }

这个改进可以解决前面提到的 x 析构后被调用的问题,但同时也引入了另一个问题: x 永远不会析构了,内存泄漏还算小问题或者说不算问题,但如果 x 的析构函数还有事情要做,如写文件什么的,此时如果不析构,显然程序的正确性都无法保证。

Nifty counter.

完美一点的解决方案是 Nifty counter, 现在 GCC 采用的就是这个做法[3]。假设现在需要被别处引用的全局变量为 x, Nifty counter 的原理是通过头文件引用,在所有需要引用 x 的地方都增加一个 static 全局变量,然后在该 static 变量的构造函数里初始化我们所需要引用的全局变量 x,在其析构函数里再清理 x,示例如下:

// global.h #ifndef _global_h_ #define _global_h_ extern X x; class initializer { public: initializer() { if (s_counter_++ == 0) init(); } ~initializer() { if (--s_counter_ == 0) clean(); } private: void init(); void clean(); static int s_counter_; }; static initializer s_init_val; #endif

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/82991f6a29eea2f45946c30b2c354f07.html