C/C++ --- 全局变量初始化总结

2023-11-11

注意:本文所说的全局变量指的是 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 在被使用前就被初始化了呢? 有如下几个技巧可以介绍一下。

(1)Construct On First Use

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

int get_global_x()
{
   static X x;
   return x.Value();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

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

(2)一个改进的做法是把静态变量改为如下的静态指针:
int get_global_x()
{
   static X* x = new X;
   return x->Value();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

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

(3)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();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

代码比较直白,所有需要引用 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();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

其中 static X& x = reinterpret_cast

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

转载自:http://blog.csdn.net/zhufenglonglove/article/details/52401939

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C/C++ --- 全局变量初始化总结 的相关文章

  • C++ 维护子类对象的混合集合

    如果我在这里错过了一个相当基本的概念 我很抱歉 但我正在尝试弄清楚如何维护多个类类型的集合 所有类类型都派生自同一个父类 并且在检索它们时仍然可以访问它们的特定于子类的方法从集合中 作为上下文 我有一个基类 BaseClass 和许多类 例
  • 静态只读字符串数组

    我在我的 Web 应用程序中使用静态只读字符串数组 基本上数组有错误代码 我将所有类似的错误代码保存在一个数组中并检查该数组 而不是检查不同常量字符串中的每个错误代码 like public static readonly string m
  • 如何填充 ToolStripComboBox?

    我发现它很难将数据绑定到ToolStripComboBox 好像没有这个ValueMember and DisplayMember特性 怎么绑定呢 访问toolstripcombobox中包装的组合框并访问其ValueMember Disp
  • 查看 NuGet 包依赖关系层次结构

    有没有一种方法 文本或图形 来查看 NuGet 包之间的依赖关系层次结构 如果您使用的是新的 csproj 您可以在此处获取所有依赖项 在项目构建后 项目目录 obj project assets json
  • 从客户端访问 DomainService 中的自定义对象

    我正在使用域服务从 Silverlight 客户端的数据库中获取数据 在DomainService1 cs中 我添加了以下内容 EnableClientAccess public class Product public int produ
  • 使用可变参数包类型扩展的 C++ 函数调用者包装器

    我绑定了一些 API 并且绑定了一些函数签名 如下所示 static bool WrapperFunction JSContext cx unsigned argc JS Value vp 我尝试将对象和函数包装在 SpiderMonkey
  • 对 std::vector 进行排序但忽略某个数字

    我有一个std vector
  • 告诉 Nancy 将枚举序列化为字符串

    Nancy 默认情况下在生成 JSON 响应时将枚举序列化为整数 我需要将枚举序列化为字符串 有一种方法可以通过创建来自定义 Nancy 的 JSON 序列化JavaScript 原始转换器 https github com NancyFx
  • 启动时的 Excel 加载项

    我正在使用 Visual C 创建 Microsoft Excel 的加载项 当我第一次创建解决方案时 它包含一个名为 ThisAddIn Startup 的函数 我在这个函数中添加了以下代码 private void ThisAddIn
  • 识别 Visual Studio 中的重载运算符 (c++)

    有没有办法使用 Visual Studio 快速直观地识别 C 中的重载运算符 在我看来 C 中的一大问题是不知道您正在使用的运算符是否已重载 Visual Studio 或某些第三方工具中是否有某些功能可以自动突出显示重载运算符或对重载运
  • 在视口中查找 WPF 控件

    Updated 这可能是一个简单或复杂的问题 但在 wpf 中 我有一个列表框 我用一个填充数据模板从列表中 有没有办法找出特定的数据模板项位于视口中 即我已滚动到其位置并且可以查看 目前我连接到了 listbox ScrollChange
  • 保护 APK 中的字符串

    我正在使用 Xamarin 的 Mono for Android 开发一个 Android 应用程序 我目前正在努力使用 Google Play API 添加应用内购买功能 为此 我需要从我的应用程序内向 Google 发送公共许可证密钥
  • C++ 中的双精度型数字

    尽管内部表示有 17 位 但 IEE754 64 位 浮点应该正确表示 15 位有效数字 有没有办法强制第 16 位和第 17 位为零 Ref http msdn microsoft com en us library system dou
  • 在屏幕上获取字符

    我浏览了 NCurses 函数列表 似乎找不到返回已打印在屏幕上的字符的函数 每个字符单元格中存储的字符是否有可访问的值 如果没有的话Windows终端有类似的功能吗 我想用它来替换屏幕上某个值的所有字符 例如 所有a s 具有不同的特征
  • String.Empty 与 "" [重复]

    这个问题在这里已经有答案了 可能的重复 String Empty 和 有什么区别 https stackoverflow com questions 151472 what is the difference between string
  • 这个可变参数模板示例有什么问题?

    基类是 include
  • 我可以在“字节数”设置为零的情况下调用 memcpy() 和 memmove() 吗?

    当我实际上没有什么可以移动 复制的时候 我是否需要处理这些情况memmove memcpy 作为边缘情况 int numberOfBytes if numberOfBytes 0 memmove dest source numberOfBy
  • 为boost python编译的.so找不到模块

    我正在尝试将 C 代码包装到 python 中 只需一个类即可导出两个函数 我编译为map so 当我尝试时import map得到像噪音一样的错误 Traceback most recent call last File
  • 如何使用 C++11 using 语法键入定义函数指针?

    我想写这个 typedef void FunctionPtr using using 我该怎么做呢 它具有类似的语法 只不过您从指针中删除了标识符 using FunctionPtr void 这是一个Example http ideone
  • 如何将十六进制字符串转换为无符号长整型?

    我有以下十六进制值 CString str str T FFF000 如何将其转换为unsigned long 您可以使用strtol作用于常规 C 字符串的函数 它使用指定的基数将字符串转换为 long long l strtol str

随机推荐