0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

從一次字符串拼接失敗說起

CPP開發(fā)者 ? 來源:CPP開發(fā)者 ? 2023-05-15 14:30 ? 次閱讀

幾個(gè)月前的時(shí)候,有一次討論,關(guān)于單例模式實(shí)現(xiàn)的,其中,提到了一種使用static方式,也就是Scott Meyers提出的另一種更優(yōu)雅的單例模式實(shí)現(xiàn),俗稱Scott Meyers單例模式。當(dāng)時(shí)聊到的一個(gè)關(guān)鍵點(diǎn)是靜態(tài)變量的初始化線程安全問題,今天借助本文,聊聊靜態(tài)變量的另外一個(gè)問題:靜態(tài)變量初始化順序。

從一個(gè)示例開始

首先看下如下代碼:

static_test.h

#include

externstd::stringstr;

static_test.cc

std::stringstr="test";

main.cc

#include"static_test.h"
#include

staticstd::stringmsg="hello"+str+"world!";

intmain(){
std::cout<

好了,在閱讀下文之前,不妨先思考下,main()函數(shù)中的輸出結(jié)果是什么,很多人第一反應(yīng)是hello test world!,恭喜你,跟我一樣,答錯(cuò)了~~~

現(xiàn)在看下編譯器的結(jié)果:

g++-gstatic_test.ccmain.cc-ostatic_test&&./static_test
helloworld

沒錯(cuò),編譯器的輸出結(jié)果是hello world!

之所以編譯器的輸出與我們的預(yù)期不一致,是因?yàn)殪o態(tài)變量初始化順序?qū)е隆?/p>

初始化

我們知道,對(duì)于已經(jīng)初始化的全局和靜態(tài)變量時(shí)存放在可執(zhí)行文件的數(shù)據(jù)段(.data),對(duì)于未初始化的全局和靜態(tài)變量,則在BSS段中。如果對(duì)這塊沒有做過深入的研究,往往很容易出錯(cuò),先看下示例:

structTest{
inti;
Test(intii):i(ii){}
Test(){}
};

Testt1=Test(5);
Testt2;
staticTestt3;
staticTestt4{5};

inti=1;
intj;
staticintk;
staticintl=1;


intmain(){
return0;
}

相信很多人看了上面代碼后給出的答案會(huì)是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段,還有一個(gè)比較有意思的點(diǎn)就是**.bss段的數(shù)據(jù)都被0進(jìn)行初始化**,針對(duì)這兩個(gè)問題:

?t1 t2 t3 t4都調(diào)用了構(gòu)造函數(shù)(有些是拷貝有些是默認(rèn)構(gòu)造函數(shù))進(jìn)行了初始化,但因?yàn)槠漕愋筒皇荘OD,所以其被放在bss段

?編譯器默認(rèn)的編譯選項(xiàng)是**-fzero-initialized-in-bss,即對(duì)bss段進(jìn)行0初始化,如果不想進(jìn)行0初始化,可以使用-fno-zero-initialized-in-bss**

針對(duì)上面的輸出,i、l在.data段,可稱之為常量初始化,而其它變量在.bss段且被0初始化,稱之為0初始化。從可執(zhí)行程序的角度來說,如果一個(gè)數(shù)據(jù)未被初始化,就不需要為其分配空間,所以.data 和.bss 的區(qū)別就是 .bss 并不占用可執(zhí)行文件的大小,僅僅記錄需要用多少空間來存儲(chǔ)這些未初始化的數(shù)據(jù),而不分配實(shí)際空間,編譯器往往通過memset(bss_str, len, 0)進(jìn)行初始化,類似于如下這種:

staticvoidzero_fill_bss(void)
externchar__START_BSS[];
externchar__END_BSS[];

memset(__START_BSS,0,(__END_BSS-__START_BSS));
}

看到這,可能大家會(huì)有個(gè)疑問,.bss段什么時(shí)候會(huì)進(jìn)行真正的初始化呢?記得一開始接觸全局變量和靜態(tài)變量的時(shí)候,書上就有提到,在可執(zhí)行程序執(zhí)行之前(main函數(shù)運(yùn)行之前),會(huì)進(jìn)行一些初始化操作,.bss就是在這個(gè)階段進(jìn)行初始化的。也就是說.data和.bss段的數(shù)據(jù),在main()函數(shù)執(zhí)行之前就初始化完成,那么,可以得出的結(jié)論是這部分?jǐn)?shù)據(jù)不存在多線程競(jìng)爭(zhēng)的問題(main()函數(shù)執(zhí)行前還不存在多線程現(xiàn)象)。

根據(jù)標(biāo)準(zhǔn)的定義:

Together, zero-initialization and constant initialization are called static initialization; all other initialization isdynamic initialization.

也就是說要將靜態(tài)變量活全局變量初始化分類的話,可以分為靜態(tài)初始化動(dòng)態(tài)初始化,其中靜態(tài)初始化已經(jīng)在上面例子中講到,就是說編譯器在編譯的過程中完成(包括常量初始化和0初始化兩種),剩下的就是動(dòng)態(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

動(dòng)態(tài)初始化,又稱為運(yùn)行時(shí)初始化或者懶漢式初始化,是指在程序運(yùn)行階段才能完成的初始化,比如動(dòng)態(tài)分配的內(nèi)存,通過函數(shù)參數(shù)進(jìn)行初始化賦值,或者使用函數(shù)返回值初始化等等,常見于函數(shù)調(diào)用方式,如下:

intfun(){
staticinta=0;
returna;
}

intmain(){
intx=fun();
return0;
}

初始化順序

在上一節(jié)中,我們聊到了編譯器對(duì)靜態(tài)變量的初始化相關(guān)知識(shí)點(diǎn),c++標(biāo)準(zhǔn)規(guī)定,在同一個(gè)編譯單元中,對(duì)全局變量或者靜態(tài)變量的初始化順序與其定義順序一致。但是對(duì)于不同的編譯單元中的靜態(tài)變量的初始化順序,標(biāo)準(zhǔn)沒有做規(guī)定,也就是說假如兩個(gè)全局靜態(tài)變量A和B分別存在與兩個(gè).cc文件中,那么編譯器對(duì)于這倆的初始化順序是不確定的,而正是因?yàn)檫@個(gè)原因,才是導(dǎo)致了文章開頭示例的輸出結(jié)果不符合語氣的關(guān)鍵。對(duì)于這種因?yàn)椴煌幾g單元初始化順序?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ù)之前,進(jìn)行初始化操作,因?yàn)闆]有規(guī)定不同編譯單元中的初始化順序,所以先初始化main.cc中的靜態(tài)變量msg為hello world!(因?yàn)榇藭r(shí)static_test.cc中的str還未進(jìn)行初始化),然后再初始化static_test.cc中的靜態(tài)變量。接著執(zhí)行main()函數(shù),進(jìn)行輸出操作...

解決

既然出現(xiàn)了因?yàn)椴煌幾g單元中的靜態(tài)變量初始化導(dǎo)致,那么就需要針對(duì)性的解決這個(gè)問題,通常有如下幾個(gè)方案:

?將所有的靜態(tài)全局變量放在一個(gè)編譯單元中(如果涉及到依賴的話,需要修改順序)

?強(qiáng)制編譯器在編譯階段進(jìn)行初始化,通常有constexprconstinit兩種

?Initialization On First Use,即在使用時(shí)候,通過函數(shù)獲取靜態(tài)對(duì)象的方式進(jìn)行初始化:

//static_test.h
#include

staticstd::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)先級(jí)(即順序,以下實(shí)現(xiàn)僅限于gcc,msvc未做研究):

//static_test.h
#include

staticstd::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)先級(jí)300,msg的優(yōu)先級(jí)400,那么在執(zhí)行的時(shí)候,會(huì)先初始化str,然后初始化msg,這樣就會(huì)得到預(yù)期結(jié)果。

結(jié)語

靜態(tài)變量在程序中使用很常見,其引起的靜態(tài)初始化順序難題也就隨之而來,對(duì)于這種初始化順序?qū)е碌漠惓?,通過很難察覺,由于標(biāo)準(zhǔn)沒有規(guī)定執(zhí)行標(biāo)準(zhǔn),因此編譯器往往也不會(huì)給出報(bào)錯(cuò)或者警告。所以,在寫代碼的時(shí)候,應(yīng)該避免這種情況的發(fā)生,當(dāng)有時(shí)候不得不使用靜態(tài)變量的時(shí)候,需要注意是否會(huì)導(dǎo)致初始化順序問題,如果遇到了,則開源參考上一節(jié)的解決方式~~

審核編輯:湯梓紅

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 字符串
    +關(guān)注

    關(guān)注

    1

    文章

    567

    瀏覽量

    20443
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4263

    瀏覽量

    62246
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4700

    瀏覽量

    68110
  • 編譯器
    +關(guān)注

    關(guān)注

    1

    文章

    1608

    瀏覽量

    48981
  • 靜態(tài)變量
    +關(guān)注

    關(guān)注

    0

    文章

    13

    瀏覽量

    6638

原文標(biāo)題:從一次字符串拼接失敗說起

文章出處:【微信號(hào):CPP開發(fā)者,微信公眾號(hào):CPP開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    labview在循環(huán)中顯示字符串,當(dāng)字符串為空時(shí),保持上一次的值,不顯示空字符串

    在循環(huán)中顯示字符串,當(dāng)字符串為空時(shí),保持上一次的值,不顯示空字符串
    發(fā)表于 10-19 17:25

    字符串的表示

    字符串的表示  隨著計(jì)算機(jī)在文字處理與信息管理中的廣泛應(yīng)用, 字符串已成為最常用的數(shù)據(jù)類型之, 許多計(jì)算機(jī)中都提供字符串操作功能, 些計(jì)
    發(fā)表于 10-13 17:11 ?3042次閱讀
    <b class='flag-5'>字符串</b>的表示

    mysql運(yùn)行拼接字符串和導(dǎo)出數(shù)據(jù)

    “prepare+execute” 學(xué)習(xí)存儲(chǔ)過程中發(fā)現(xiàn)sql語句有些部分不能夠使用變量,因此采用拼接字符串的形式,然后執(zhí)行字符串代表的SQL?;拘问饺缦拢?set @ sql =concat
    發(fā)表于 11-28 20:37 ?1074次閱讀

    python字符串拼接方式了解

    python字符串拼接的方式 在Python的實(shí)際開發(fā)中,很多都需要用到字符串拼接,python中字符串
    發(fā)表于 12-06 10:09 ?1013次閱讀

    什么是復(fù)制字符串?Python如何復(fù)制字符串

    連續(xù)幾篇文章都在寫 Python 字符串,這出乎我的意料了。但是,有的問題,不寫不行,特別是那種靈機(jī)動(dòng)想到的問題,最后你發(fā)現(xiàn),很多人根本不懂卻又誤以為自己懂了。那就繼續(xù)刨根問底,探究個(gè)明白吧
    發(fā)表于 11-25 10:32 ?2964次閱讀

    詳解Python如何拼接字符串

    占位符,它僅代表字符串,并不是拼接的實(shí)際內(nèi)容。實(shí)際的拼接內(nèi)容在個(gè)單獨(dú)的%號(hào)后面,放在個(gè)元
    發(fā)表于 11-26 11:16 ?1020次閱讀

    字符串函數(shù)重寫練習(xí)

    字符串函數(shù)重寫練習(xí):字符串比較、字符串拼接字符串查找、字符串拷貝、內(nèi)存比較、內(nèi)存拷貝、內(nèi)存初始
    的頭像 發(fā)表于 05-05 15:02 ?1928次閱讀

    C語言總結(jié)_字符串函數(shù)封裝練習(xí)

    字符串函數(shù)重寫練習(xí):字符串比較、字符串拼接、字符串查找、字符串拷貝、內(nèi)存比較、內(nèi)存拷貝、內(nèi)存初始
    的頭像 發(fā)表于 08-14 09:42 ?934次閱讀

    labview字符串控件二封裝

    Labiew字符串控件二封裝,用起來方便的很.
    發(fā)表于 11-14 15:14 ?4次下載

    文詳解JavaScript字符串

    JavaScript字符串是原始值。此外,字符串是不可變的。這意味著如果你修改一個(gè)字符串,你總是會(huì)得到個(gè)新的字符串。原始
    的頭像 發(fā)表于 12-08 16:36 ?1127次閱讀

    字符串的相關(guān)知識(shí)

    TCL 中的數(shù)據(jù)類型只有種:字符串。這些字符串可以是字母、數(shù)字、布爾值、標(biāo)點(diǎn)符號(hào)等特殊字符的組合。在某些特殊命令的作用下,字符串可以向其他
    的頭像 發(fā)表于 03-29 11:41 ?1038次閱讀

    python輸出固定長(zhǎng)度的字符串

    Python 是種強(qiáng)大而靈活的編程語言,具有許多用于處理字符串的功能。在 Python 中,有多種方法可以輸出固定長(zhǎng)度的字符串。下面將詳細(xì)介紹這些方法。 方法:使用
    的頭像 發(fā)表于 11-22 10:41 ?2877次閱讀

    oracle中拼接字符串函數(shù)

    , string2) 其中,string1 和 string2 是需要連接的字符串參數(shù)。 除了 CONCAT 函數(shù),Oracle 還提供了些其他的字符串拼接函數(shù)和操作符,這些函數(shù)和操
    的頭像 發(fā)表于 12-06 09:49 ?2656次閱讀

    oracle拼接字符串函數(shù)wm_con

    在Oracle數(shù)據(jù)庫中,有時(shí)候我們需要將多個(gè)字符串拼接一個(gè)字符串,以滿足特定的需求。而Oracle提供了個(gè)非常方便的函數(shù),就是WM_CONCAT函數(shù)。本文將詳細(xì)介紹WM_CONCA
    的頭像 發(fā)表于 12-06 09:51 ?1323次閱讀

    labview掃描字符串怎么用

    介紹如何在 LabVIEW 中使用掃描字符串以及相關(guān)的技巧和注意事項(xiàng)。 字符串是 LabVIEW 中的種基本數(shù)據(jù)類型,表示系列字符的序列
    的頭像 發(fā)表于 12-29 10:12 ?1715次閱讀