0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

从一次字符串拼接失败说起

CPP开发者 来源:CPP开发者 2023-05-15 14:30 次阅读

几个月前的时候,有一次讨论,关于单例模式实现的,其中,提到了一种使用static方式,也就是Scott Meyers提出的另一种更优雅的单例模式实现,俗称Scott Meyers单例模式。当时聊到的一个关键点是静态变量的初始化线程安全问题,今天借助本文,聊聊静态变量的另外一个问题:静态变量初始化顺序

从一个示例开始

首先看下如下代码:

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<< msg << std::endl;
}

好了,在阅读下文之前,不妨先思考下,main()函数中的输出结果是什么,很多人第一反应是hello test world!,恭喜你,跟我一样,答错了~~~

现在看下编译器的结果:

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

没错,编译器的输出结果是hello world!

之所以编译器的输出与我们的预期不一致,是因为静态变量初始化顺序导致。

初始化

我们知道,对于已经初始化的全局和静态变量时存放在可执行文件的数据段(.data),对于未初始化的全局和静态变量,则在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。在给出答案之前,不妨看下编译器的输出结果:

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段的数据都被0进行初始化**,针对这两个问题:

•t1 t2 t3 t4都调用了构造函数(有些是拷贝有些是默认构造函数)进行了初始化,但因为其类型不是POD,所以其被放在bss段

•编译器默认的编译选项是**-fzero-initialized-in-bss,即对bss段进行0初始化,如果不想进行0初始化,可以使用-fno-zero-initialized-in-bss**

针对上面的输出,i、l在.data段,可称之为常量初始化,而其它变量在.bss段且被0初始化,称之为0初始化。从可执行程序的角度来说,如果一个数据未被初始化,就不需要为其分配空间,所以.data 和.bss 的区别就是 .bss 并不占用可执行文件的大小,仅仅记录需要用多少空间来存储这些未初始化的数据,而不分配实际空间,编译器往往通过memset(bss_str, len, 0)进行初始化,类似于如下这种:

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

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

看到这,可能大家会有个疑问,.bss段什么时候会进行真正的初始化呢?记得一开始接触全局变量和静态变量的时候,书上就有提到,在可执行程序执行之前(main函数运行之前),会进行一些初始化操作,.bss就是在这个阶段进行初始化的。也就是说.data和.bss段的数据,在main()函数执行之前就初始化完成,那么,可以得出的结论是这部分数据不存在多线程竞争的问题(main()函数执行前还不存在多线程现象)。

根据标准的定义:

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

也就是说要将静态变量活全局变量初始化分类的话,可以分为静态初始化动态初始化,其中静态初始化已经在上面例子中讲到,就是说编译器在编译的过程中完成(包括常量初始化和0初始化两种),剩下的就是动态初始化:

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

动态初始化,又称为运行时初始化或者懒汉式初始化,是指在程序运行阶段才能完成的初始化,比如动态分配的内存,通过函数参数进行初始化赋值,或者使用函数返回值初始化等等,常见于函数调用方式,如下:

intfun(){
staticinta=0;
returna;
}

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

初始化顺序

在上一节中,我们聊到了编译器对静态变量的初始化相关知识点,c++标准规定,在同一个编译单元中,对全局变量或者静态变量的初始化顺序与其定义顺序一致。但是对于不同的编译单元中的静态变量的初始化顺序,标准没有做规定,也就是说假如两个全局静态变量A和B分别存在与两个.cc文件中,那么编译器对于这俩的初始化顺序是不确定的,而正是因为这个原因,才是导致了文章开头示例的输出结果不符合语气的关键。对于这种因为不同编译单元初始化顺序导致的异常,cppreference将其称之为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.

继续回到文章开头的示例,在程序执行main()函数之前,进行初始化操作,因为没有规定不同编译单元中的初始化顺序,所以先初始化main.cc中的静态变量msg为hello world!(因为此时static_test.cc中的str还未进行初始化),然后再初始化static_test.cc中的静态变量。接着执行main()函数,进行输出操作...

解决

既然出现了因为不同编译单元中的静态变量初始化导致,那么就需要针对性的解决这个问题,通常有如下几个方案:

•将所有的静态全局变量放在一个编译单元中(如果涉及到依赖的话,需要修改顺序)

•强制编译器在编译阶段进行初始化,通常有constexprconstinit两种

•Initialization On First Use,即在使用时候,通过函数获取静态对象的方式进行初始化:

//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<< msg << std::endl;
 }

•指定初始化优先级(即顺序,以下实现仅限于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<< msg << std::endl;
  }

在上述代码中指定了静态变量str的优先级300,msg的优先级400,那么在执行的时候,会先初始化str,然后初始化msg,这样就会得到预期结果。

结语

静态变量在程序中使用很常见,其引起的静态初始化顺序难题也就随之而来,对于这种初始化顺序导致的异常,通过很难察觉,由于标准没有规定执行标准,因此编译器往往也不会给出报错或者警告。所以,在写代码的时候,应该避免这种情况的发生,当有时候不得不使用静态变量的时候,需要注意是否会导致初始化顺序问题,如果遇到了,则开源参考上一节的解决方式~~

审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 字符串
    +关注

    关注

    1

    文章

    579

    浏览量

    20511
  • 函数
    +关注

    关注

    3

    文章

    4329

    浏览量

    62588
  • 代码
    +关注

    关注

    30

    文章

    4786

    浏览量

    68563
  • 编译器
    +关注

    关注

    1

    文章

    1634

    浏览量

    49122
  • 静态变量
    +关注

    关注

    0

    文章

    13

    浏览量

    6649

原文标题:从一次字符串拼接失败说起

文章出处:【微信号:CPP开发者,微信公众号:CPP开发者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    labview在循环中显示字符串,当字符串为空时,保持上一次的值,不显示空字符串

    在循环中显示字符串,当字符串为空时,保持上一次的值,不显示空字符串
    发表于 10-19 17:25

    字符串的表示

    字符串的表示  随着计算机在文字处理与信息管理中的广泛应用, 字符串已成为最常用的数据类型之, 许多计算机中都提供字符串操作功能, 些计
    发表于 10-13 17:11 3066次阅读
    <b class='flag-5'>字符串</b>的表示

    mysql运行拼接字符串和导出数据

    “prepare+execute” 学习存储过程中发现sql语句有些部分不能够使用变量,因此采用拼接字符串的形式,然后执行字符串代表的SQL。基本形式如下: set @ sql =concat
    发表于 11-28 20:37 1097次阅读

    python字符串拼接方式了解

    python字符串拼接的方式 在Python的实际开发中,很多都需要用到字符串拼接,python中字符串
    发表于 12-06 10:09 1034次阅读

    什么是复制字符串?Python如何复制字符串

    连续几篇文章都在写 Python 字符串,这出乎我的意料了。但是,有的问题,不写不行,特别是那种灵机动想到的问题,最后你发现,很多人根本不懂却又误以为自己懂了。那就继续刨根问底,探究个明白吧
    发表于 11-25 10:32 3017次阅读

    详解Python如何拼接字符串

    占位符,它仅代表字符串,并不是拼接的实际内容。实际的拼接内容在个单独的%号后面,放在个元
    发表于 11-26 11:16 1045次阅读

    字符串函数重写练习

    字符串函数重写练习:字符串比较、字符串拼接字符串查找、字符串拷贝、内存比较、内存拷贝、内存初始
    的头像 发表于 05-05 15:02 1996次阅读

    C语言总结_字符串函数封装练习

    字符串函数重写练习:字符串比较、字符串拼接字符串查找、字符串拷贝、内存比较、内存拷贝、内存初始
    的头像 发表于 08-14 09:42 998次阅读

    labview字符串控件二封装

    Labiew字符串控件二封装,用起来方便的很.
    发表于 11-14 15:14 4次下载

    文详解JavaScript字符串

    JavaScript字符串是原始值。此外,字符串是不可变的。这意味着如果你修改一个字符串,你总是会得到个新的字符串。原始
    的头像 发表于 12-08 16:36 1204次阅读

    字符串的相关知识

    TCL 中的数据类型只有种:字符串。这些字符串可以是字母、数字、布尔值、标点符号等特殊字符的组合。在某些特殊命令的作用下,字符串可以向其他
    的头像 发表于 03-29 11:41 1150次阅读

    python输出固定长度的字符串

    Python 是种强大而灵活的编程语言,具有许多用于处理字符串的功能。在 Python 中,有多种方法可以输出固定长度的字符串。下面将详细介绍这些方法。 方法:使用
    的头像 发表于 11-22 10:41 3315次阅读

    oracle中拼接字符串函数

    , string2) 其中,string1 和 string2 是需要连接的字符串参数。 除了 CONCAT 函数,Oracle 还提供了些其他的字符串拼接函数和操作符,这些函数和操
    的头像 发表于 12-06 09:49 2934次阅读

    oracle拼接字符串函数wm_con

    在Oracle数据库中,有时候我们需要将多个字符串拼接一个字符串,以满足特定的需求。而Oracle提供了个非常方便的函数,就是WM_CONCAT函数。本文将详细介绍WM_CONCA
    的头像 发表于 12-06 09:51 1763次阅读

    labview扫描字符串怎么用

    介绍如何在 LabVIEW 中使用扫描字符串以及相关的技巧和注意事项。 字符串是 LabVIEW 中的种基本数据类型,表示系列字符的序列
    的头像 发表于 12-29 10:12 2010次阅读