c++ 全局變量初始化的一點總結

注意:本文所說的全局變量指的是 variables with static storage,措詞來自 c++ 的語言標準文檔。html

何時初始化

根據 C++ 標準,全局變量的初始化要在 main 函數執行前完成,常識無疑,可是這個說法有點含糊,main 函數執行前到底具體是何時呢?是編譯時仍是運行時?答案是既有編譯時,也可能會有運行時(seriously), 從語言的層面來講,全局變量的初始化能夠劃分爲如下兩個階段(c++11 N3690 3.6.2):ios

  1. static initialization: 靜態初始化指的是用常量來對變量進行初始化,主要包括 zero initialization 和 const initialization,靜態初始化在程序加載的過程當中完成,對簡單類型(內建類型,POD等)來講,從具體實現上看,zero initialization 的變量會被保存在 bss 段,const initialization 的變量則放在 data 段內,程序加載便可完成初始化,這和 c 語言裏的全局變量初始化基本是一致的。c++

  2. dynamic initialization:動態初始化主要是指須要通過函數調用才能完成的初始化,好比說:int a = foo(),或者是複雜類型(類)的初始化(須要調用構造函數)等。這些變量的初始化會在 main 函數執行前由運行時調用相應的代碼從而得以進行(函數內的 static 變量除外)。函數

須要明確的是:靜態初始化執行先於動態初始化! 只有當全部靜態初始化執行完畢,動態初始化纔會執行。顯然,這樣的設計是很直觀的,能靜態初始化的變量,它的初始值都是在編譯時就能肯定,所以能夠直接 hard code 到生成的代碼裏,而動態初始化須要在運行時執行相應的動做才能進行,所以,靜態初始化先於動態初始化是必然的。設計

初始化的順序

對於出如今同一個編譯單元內的全局變量來講,它們初始化的順序與他們聲明的順序是一致的(銷燬的順序則反過來),而對於不一樣編譯單元間的全局變量,c++ 標準並無明確規定它們之間的初始化(銷燬)順序應該怎樣,所以實現上徹底由編譯器本身決定,一個比較廣泛的認識是:不一樣編譯單元間的全局變量的初始化順序是不固定的,哪怕對同一個編譯器,同一份代碼來講,任意兩次編譯的結果都有可能不同[1]。指針

所以,一個很天然的問題就是,若是不一樣編譯單元間的全局變量相互引用了怎麼辦?c++11

固然,最好的解決方法是儘量的避免這種狀況(防治勝於治療嘛),由於通常來講,若是出現了全局變量引用全局變量的窘況,那多半是程序自己的設計出了問題,此時最應該作的是回頭從新思考和修改程序的結構與實現,而不是急着窮盡技巧來給錯誤的設計打補丁。code

---- 說得輕鬆。htm

幾個技巧

好吧,我認可總有那麼一些特殊的狀況,是須要咱們來處理這種在全局變量的初始化函數里居然引用了別的地方的全局變量的狀況,好比說在全局變量的初始化函數裏調用了 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

相關文章
相關標籤/搜索