c++ 全局变量初始化的一点总结
注意:本文所说的全局变量指的是 variables with static storage,措词来自 c++ 的语言标准文档。
什么时候初始化
根据 C++ 标准,全局变量的初始化要在 main 函数执行前完成,常识无疑,但是这个说法有点含糊,main 函数执行前到底具体是什么时候呢?是编译时还是运行时?答案是既有编译时,也可能会有运行时(seriously), 从语言的层面来说,全局变量的初始化可以划分为以下两个阶段(c++11 N3690 3.6.2):
static initialization: 静态初始化指的是用常量来对变量进行初始化,主要包括 zero initialization 和 const initialization,静态初始化在程序加载的过程中完成,对简单类型(内建类型,POD等)来说,从具体实现上看,zero initialization 的变量会被保存在 bss 段,const initialization 的变量则放在 data 段内,程序加载即可完成初始化,这和 c 语言里的全局变量初始化基本是一致的。
dynamic initialization:动态初始化主要是指需要经过函数调用才能完成的初始化,比如说:
int a = foo(),或者是复杂类型(类)的初始化(需要调用构造函数)等。这些变量的初始化会在 main 函数执行前由运行时调用相应的代码从而得以进行(函数内的 static 变量除外)。
需要明确的是:静态初始化执行先于动态初始化! 只有当所有静态初始化执行完毕,动态初始化才会执行。显然,这样的设计是很直观的,能静态初始化的变量,它的初始值都是在编译时就能确定,因此可以直接 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][7]。假设现在需要被别处引用的全局变量为 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
相应的 cpp 文件:
// global.cpp
#include "global.h"
static X x;
int initializer::s_counter_ = 0;
void initializer::init()
{
new(&x) X;
}
void initializer::clean()
{
(&x)->~X();
}
代码比较直白,所有需要引用 x 的地方都需要引用 global.h 这个头文件,而一旦引入了该头文件,就一定会引入 initializer 类型的一个静态变量 s_init_val, 因此虽然不同编译单元间的初始化顺序不确定,但他们都肯定包含有 s_init_val,因此我们可以在 s_init_val 的构造函数里加入对 x 的初始化操作,只有在第一个 s_init_val 被构造时才初始化 x 变量,这可以通过 initializer 的静态成员变量来实现,因为 s_counter_ 的初始化是静态初始化,能保证在程序加载后就完成了。
初始化 x 用到了 placement new 的技巧,至于析构,那就是简单粗暴地直接调用析构函数了,这一段代码里的技巧也许有些难看,但都是合法的,当然,同时还有些问题待解决:
首先,因为 x 是复杂类型的变量,它有自己的构造函数,init() 函数初始化 x 之后,程序初始化 x 所在的编译单元时,x 的构造函数还会被再调用一次,同理 x 析构函数也会被调用两次,这显然很容易引起问题,解决的方法是把 x 改为引用:
// global.cpp
#include "global.h"
// need to ensure memory alignment??
static char g_dummy[sizeof(X)];
static X& x = reinterpret_cast<X&>(g_dummy);
int initializer::s_counter_ = 0;
void initializer::init()
{
new(&x) X;
}
void initializer::clean()
{
(&x)->~X();
}
其中 static X& x = reinterpret_cast<X&>(g_dummy); 这一行是静态初始化,因为 g_dummy 是编译时就确定了的(引用是简单类型且以常量为初始值),而 x 只是一个强制转化而来的引用,编译器不会生成调用 x 构造函数和析构函数的代码。通过上面的修改,这个方案已经比较完美了,但遗憾的是它也不是 100% 正确的,这个方案能正确工作的前提是:所有引用 x 的地方都会 include 头文件 global.h,但如果某一个全局变量 y 的初始化函数里没有直接引用 x, 而是间接调用了另一个函数 foo,再通过 foo 引用了 x,此时就可能出错了,因为 y 所在的编译单元里可能并没有直接引用 x,因此很有可能就没有 include 头文件 global.h,那么 y 的初始化就很有可能发生在 x 之前。。。
这个问题在 gcc c++ 的标准库里也没有得到解决,有兴趣的可以看看这个讨论。
[参考]
[1] http://isocpp.org/wiki/faq/ctors#static-init-order
[2] https://gcc.gnu.org/onlinedocs/libstdc++/manual/io.html#std.io.objects
[3] https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-3.4/ios__init_8cc-source.html
[4] https://social.msdn.microsoft.com/Forums/vstudio/en-US/637a4c27-3e30-4b88-b36d-b5b720cf0d04/why-are-cout-cin-initialized-once-and-only-once-given-the-scheme-below-in-the-iostream?forum=vclanguage
[5] http://www.petebecker.com/js/js199905.html
[6] http://blogs.msdn.com/b/ce_base/archive/2008/06/02/dynamic-initialization-of-variables.aspx
[7] http://cs.brown.edu/people/jwicks/libstdc++/html_user/globals__io_8cc-source.html
c++ 全局变量初始化的一点总结的更多相关文章
- VC++全局变量初始化
目录 第1章说明 2 1.1 程序启动 2 1.2 强符号.弱符号 2 1.3 动态初始化顺序 3 1.4 exe调用dll 4 1.5 禁用动态初始化 4 1.6 ...
- 一个可遇不可求的 bug 全局变量初始化顺序问题 哈哈
这是今天下午帮同事查的一个客户端 C++ 的 bug,前人留下的谜之代码.. 具体情况是,客户端实现了有一个简单的内存池,每次申请内存的时候会把新申请到的内存信息存到一个 map 里,据说是为了检查内 ...
- ServletContext全局变量初始化
Java部分 package com.servlet; import java.io.IOException; import java.io.PrintWriter; import javax.ser ...
- c语言中字符串数组初始化的一点总结&& c++访问控制的三种方式
char *c[]={"ONE","TWO","THREE","FOUR"}; // c语言中定义了一个字符串数组(也称 ...
- [转]C/C++关于全局变量和局部变量初始化与不初始化的区别
原文链接:http://www.kingofcoders.com/viewNews.php?type=newsCpp&id=189&number=4836955386 在C语言里,全局 ...
- dll加载过程全局变量会先初始化
在一个生成dll的工程中看到一个文件只有一句全局变量初始化的代码,很好奇为什么这句代码在dll加载的时候就会执行,因此断点调试发现 __declspec(noinline) BOOL __cdecl ...
- ucos系统初始化及启动过程
之前在ucos多任务切换中漏掉了一个变量, OSCtxSwCtr标识系统任务切换次数 主要应该还是用在调试功能中 Ucos系统初始化函数为OSInit(),主要完成以下功能 全局变量初始化 就绪任务表 ...
- 浅谈C语言中结构体的初始化
转自:http://www.jb51.net/article/37246.htm <代码大全>建议在变量定义的时候进行初始化,但是很多人,特别是新人对结构体或者结构体数组定义是一般不会初始 ...
- caffe中权值初始化方法
首先说明:在caffe/include/caffe中的 filer.hpp文件中有它的源文件,如果想看,可以看看哦,反正我是不想看,代码细节吧,现在不想知道太多,有个宏观的idea就可以啦,如果想看代 ...
随机推荐
- .net使用FluentValidation进行服务端验证。
背景 最近使用asp.mvc 做一个在线口语系统项目,在服务端验证问题遇到了一些小问题. 自己根据数据库表user定义一个数据库表实体对象UserDbEntity [Table(" ...
- 更新日志 - BugHD Android 客户端上线
当我们讨论 Bug 的时候,总是一脸愁容.尤其是移动应用的开发者,要应对用户可能在各种场景下使用 App 时产生的莫名崩溃. 为了更好地解决开发者的焦虑,BugHD Android 客户端上线了,高效 ...
- 每天一个linux命令(6):rmdir 命令
今天学习一下linux中命令: rmdir命令.rmdir是常用的命令,该命令的功能是删除空目录,一个目录被删除之前必须是空的.(注意,rm - r dir命令可代替rmdir,但是有很大危险性.)删 ...
- 教程:使用Diskpart创建、扩展或删除磁盘分区
在Windows Server环境下进行基本的磁盘操作时,管理员可以使用Disk Partition Utility或Diskpart等工具.后者是一个命令行解释器,可作为磁盘管理工具. 管理员可以使 ...
- 用inno Setup做应用程序安装包的示例脚本(.iss文件)(
用innoSetup做应用程序安装包的示例脚本(.iss文件),具体要看innoSetup附带的文档,好象是pascal语言写的脚本. 示例1(应用程序.exe,客户端安装): ;{089D6802- ...
- java代码实现如何获取当前经纬度?(安卓的话可以用GPS取)
import android.app.Activity; import android.os.Bundle; import android.location.*; import android.con ...
- 使用commons-beanutils迭代获取javabean的属性
NoteEntity entity = new NoteEntity(); entity.setNote001("a1"); entity.setNote002("a2& ...
- Sicily 3913. 阶乘之和
http://soj.me/3913 一开始被它的数据吓到了,还以为很复杂,但想清楚之后,确实是比较简单的,你只需要算到 24! 就行了,大于 24 的时候答案永远是 940313,因为我们是对 10 ...
- java Joda-Time 对日期、时间操作
任何企业应用程序都需要处理时间问题.应用程序需要知道当前的时间点和下一个时间点,有时它们还必须计算这两个时间点之间的路径.使用 JDK 完成这项任务将非常痛苦和繁琐.现在来看看 Joda Time,一 ...
- PPTP VPN 一键安装包(图文,OpenVZ适用)[zz]
[介绍] VPN的英文全称是“Virtual Private Network”,中文名叫“虚拟专用网络”.VPN可以通过特殊加密的通讯协议连接到Internet上,在位于不同地方的两个或多个内部网之间 ...