幾個月前的時候,有一次討論,關(guān)于單例模式實現(xiàn)的,其中,提到了一種使用static方式,也就是Scott Meyers提出的另一種更優(yōu)雅的單例模式實現(xiàn),俗稱Scott Meyers單例模式。當時聊到的一個關(guān)鍵點是靜態(tài)變量的初始化線程安全問題,今天借助本文,聊聊靜態(tài)變量的另外一個問題:靜態(tài)變量初始化順序。
從一個示例開始
首先看下如下代碼:
static_test.h
#includeexternstd::stringstr;
static_test.cc
std::stringstr="test";
main.cc
#include"static_test.h" #includestaticstd::stringmsg="hello"+str+"world!"; intmain(){ std::cout<
好了,在閱讀下文之前,不妨先思考下,main()函數(shù)中的輸出結(jié)果是什么,很多人第一反應(yīng)是hello test world!,恭喜你,跟我一樣,答錯了~~~
現(xiàn)在看下編譯器的結(jié)果:
g++-gstatic_test.ccmain.cc-ostatic_test&&./static_test helloworld
沒錯,編譯器的輸出結(jié)果是hello world!
之所以編譯器的輸出與我們的預期不一致,是因為靜態(tài)變量初始化順序?qū)е隆?/p>
初始化
我們知道,對于已經(jīng)初始化的全局和靜態(tài)變量時存放在可執(zhí)行文件的數(shù)據(jù)段(.data),對于未初始化的全局和靜態(tài)變量,則在BSS段中。如果對這塊沒有做過深入的研究,往往很容易出錯,先看下示例:
structTest{ inti; Test(intii):i(ii){} Test(){} }; Testt1=Test(5); Testt2; staticTestt3; staticTestt4{5}; inti=1; intj; staticintk; staticintl=1; intmain(){ return0; }
相信很多人看了上面代碼后給出的答案會是t1 t4 i l 在.data,t2 t3 j k在.bss。在給出答案之前,不妨看下編譯器的輸出結(jié)果:
g++ test.cpp && objdump -dj .data a.out:
a.out:fileformatelf64-x86-64 Disassemblyofsection.data: 0000000000600a88<__data_start>: ... 0000000000600a90<__dso_handle>: ... 0000000000600a98: 600a98:01000000.... 0000000000600a9c<_ZL1l>: 600a9c:01000000....
objdump -dj .bss a.out:
a.out:fileformatelf64-x86-64 Disassemblyofsection.bss: 0000000000600aa0: ... 0000000000600aa8 : ... 0000000000600ab0 : 600ab0:00000000.... 0000000000600ab4 : 600ab4:00000000.... 0000000000600ab8 : 600ab8:00000000.... 0000000000600abc<_ZL2t3>: 600abc:00000000.... 0000000000600ac0<_ZL2t4>: 600ac0:00000000.... 0000000000600ac4<_ZL1k>: 600ac4:00000000....
從上述輸出可知只有i、l在.data段,其它的在.bss段,還有一個比較有意思的點就是**.bss段的數(shù)據(jù)都被0進行初始化**,針對這兩個問題:
?t1 t2 t3 t4都調(diào)用了構(gòu)造函數(shù)(有些是拷貝有些是默認構(gòu)造函數(shù))進行了初始化,但因為其類型不是POD,所以其被放在bss段
?編譯器默認的編譯選項是**-fzero-initialized-in-bss,即對bss段進行0初始化,如果不想進行0初始化,可以使用-fno-zero-initialized-in-bss**
針對上面的輸出,i、l在.data段,可稱之為常量初始化,而其它變量在.bss段且被0初始化,稱之為0初始化。從可執(zhí)行程序的角度來說,如果一個數(shù)據(jù)未被初始化,就不需要為其分配空間,所以.data 和.bss 的區(qū)別就是 .bss 并不占用可執(zhí)行文件的大小,僅僅記錄需要用多少空間來存儲這些未初始化的數(shù)據(jù),而不分配實際空間,編譯器往往通過memset(bss_str, len, 0)進行初始化,類似于如下這種:
staticvoidzero_fill_bss(void) externchar__START_BSS[]; externchar__END_BSS[]; memset(__START_BSS,0,(__END_BSS-__START_BSS)); }
看到這,可能大家會有個疑問,.bss段什么時候會進行真正的初始化呢?記得一開始接觸全局變量和靜態(tài)變量的時候,書上就有提到,在可執(zhí)行程序執(zhí)行之前(main函數(shù)運行之前),會進行一些初始化操作,.bss就是在這個階段進行初始化的。也就是說.data和.bss段的數(shù)據(jù),在main()函數(shù)執(zhí)行之前就初始化完成,那么,可以得出的結(jié)論是這部分數(shù)據(jù)不存在多線程競爭的問題(main()函數(shù)執(zhí)行前還不存在多線程現(xiàn)象)。
根據(jù)標準的定義:
Together, zero-initialization and constant initialization are called static initialization; all other initialization isdynamic initialization.
也就是說要將靜態(tài)變量活全局變量初始化分類的話,可以分為靜態(tài)初始化和動態(tài)初始化,其中靜態(tài)初始化已經(jīng)在上面例子中講到,就是說編譯器在編譯的過程中完成(包括常量初始化和0初始化兩種),剩下的就是動態(tài)初始化:
Dynamic initialization happens at runtime for variables that can’t be evaluated at compile time2. Here, static variables are initialized every time the executable is run and not just once during compilation
動態(tài)初始化,又稱為運行時初始化或者懶漢式初始化,是指在程序運行階段才能完成的初始化,比如動態(tài)分配的內(nèi)存,通過函數(shù)參數(shù)進行初始化賦值,或者使用函數(shù)返回值初始化等等,常見于函數(shù)調(diào)用方式,如下:
intfun(){ staticinta=0; returna; } intmain(){ intx=fun(); return0; }
初始化順序
在上一節(jié)中,我們聊到了編譯器對靜態(tài)變量的初始化相關(guān)知識點,c++標準規(guī)定,在同一個編譯單元中,對全局變量或者靜態(tài)變量的初始化順序與其定義順序一致。但是對于不同的編譯單元中的靜態(tài)變量的初始化順序,標準沒有做規(guī)定,也就是說假如兩個全局靜態(tài)變量A和B分別存在與兩個.cc文件中,那么編譯器對于這倆的初始化順序是不確定的,而正是因為這個原因,才是導致了文章開頭示例的輸出結(jié)果不符合語氣的關(guān)鍵。對于這種因為不同編譯單元初始化順序?qū)е碌漠惓#琧ppreference將其稱之為Static Initialization Order Fiasco。
Thestatic initialization order fiascorefers to the ambiguity in the order that objects with static storage duration in different translation unitsare initializedin. If an object in one translation unit relies on an object in another translation unit already being initialized, a crash can occur if the compiler decides to initialize them in the wrong order. For example, the order in which .cpp files are specified on the command line may alter this order. The Construct on First Use Idiom can be used to avoid the static initialization order fiasco and ensure that all objects are initialized in the correct order.
Within a single translation unit, the fiasco does not apply because the objects are initialized from top to bottom.
繼續(xù)回到文章開頭的示例,在程序執(zhí)行main()函數(shù)之前,進行初始化操作,因為沒有規(guī)定不同編譯單元中的初始化順序,所以先初始化main.cc中的靜態(tài)變量msg為hello world!(因為此時static_test.cc中的str還未進行初始化),然后再初始化static_test.cc中的靜態(tài)變量。接著執(zhí)行main()函數(shù),進行輸出操作...
解決
既然出現(xiàn)了因為不同編譯單元中的靜態(tài)變量初始化導致,那么就需要針對性的解決這個問題,通常有如下幾個方案:
?將所有的靜態(tài)全局變量放在一個編譯單元中(如果涉及到依賴的話,需要修改順序)
?強制編譯器在編譯階段進行初始化,通常有constexpr和constinit兩種
?Initialization On First Use,即在使用時候,通過函數(shù)獲取靜態(tài)對象的方式進行初始化:
//static_test.h #includestaticstd::stringstr; //static_test.cc std::stringGetStr(){ str="test"; reurnstr; } //main.cc #include"static_test.h" #include staticstd::stringmsg="hello"+GetStr()+"world!"; intmain(){ std::cout<
?指定初始化優(yōu)先級(即順序,以下實現(xiàn)僅限于gcc,msvc未做研究):
//static_test.h #includestaticstd::stringstr; //static_test.cc std::string__attribute__((init_priority(300)))str="test"; //main.cc #include"static_test.h" #include staticstd::string__attribute__((init_priority(400)))msg="hello"+str+"world"; intmain(){ std::cout<
在上述代碼中指定了靜態(tài)變量str的優(yōu)先級300,msg的優(yōu)先級400,那么在執(zhí)行的時候,會先初始化str,然后初始化msg,這樣就會得到預期結(jié)果。
結(jié)語
靜態(tài)變量在程序中使用很常見,其引起的靜態(tài)初始化順序難題也就隨之而來,對于這種初始化順序?qū)е碌漠惓?,通過很難察覺,由于標準沒有規(guī)定執(zhí)行標準,因此編譯器往往也不會給出報錯或者警告。所以,在寫代碼的時候,應(yīng)該避免這種情況的發(fā)生,當有時候不得不使用靜態(tài)變量的時候,需要注意是否會導致初始化順序問題,如果遇到了,則開源參考上一節(jié)的解決方式~~
審核編輯:湯梓紅
-
字符串
+關(guān)注
關(guān)注
1文章
589瀏覽量
21171 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4374瀏覽量
64385 -
代碼
+關(guān)注
關(guān)注
30文章
4891瀏覽量
70352 -
編譯器
+關(guān)注
關(guān)注
1文章
1657瀏覽量
49968 -
靜態(tài)變量
+關(guān)注
關(guān)注
0文章
13瀏覽量
6776
原文標題:從一次字符串拼接失敗說起
文章出處:【微信號:CPP開發(fā)者,微信公眾號:CPP開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
labview在循環(huán)中顯示字符串,當字符串為空時,保持上一次的值,不顯示空字符串
字符串的表示

評論