赞
踩
(
转者注:C/C++中的静态变量、全局变量在存储、初始化、赋值等问题上,有不少异于常识的处理行为;而甚至C与C++之间也会有一些区别。这给笔者带来不少困惑,经过阅读博客和结合实验,搞清楚了不少问题。下面是有关的转载。
)
一、下面这篇转自 http://www.cnblogs.com/catch/p/4314256.html 是有关初始化的。
注意:本文所说的全局变量指的是 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 在被使用前就被初始化了呢? 有如下几个技巧可以介绍一下。
该做法是把对全局变量的引用改为函数调用,然后把全局变量改为函数内的静态变量:
- 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, 现在 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
关于编译选项 -Wa,-adlhn参考
http://blog.csdn.net/lanxinju/article/details/5900986
以下内容来自于内网别的高人的回复
可以写个程序测试一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class
A
{
public
:
A() {}
public
:
int
a;
};
int
static_var_func()
{
static
A a;
return
a.a++;
}
int
main(
int
argc,
char
* argv[])
{
static_var_func();
return
0;
}
|
看看汇编
1
|
g++ -c -g -Wa,-adlhn yy.cpp
|
结果:
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
|
10:yy.cpp ****
int
static_var_func()
35 .loc 1 10 0
36 0000 55 pushq %rbp
37 .LCFI2:
38 0001 4889E5 movq %rsp, %rbp
39 .LCFI3:
40 .LBB2:
11:yy.cpp **** {
12:yy.cpp ****
static
A a;
41 .loc 1 12 0
42 0004 B8000000 movl $_ZGVZ15static_var_funcvE1a, %eax
42 00
43 0009 0FB600 movzbl (%rax), %eax
44 000c 84C0 testb %al, %al
45 000e 7527 jne .L4
46 0010 BF000000 movl $_ZGVZ15static_var_funcvE1a, %edi
46 00
47 0015 E8000000 call __cxa_guard_acquire
47 00
48 001a 85C0 testl %eax, %eax
49 001c 0F95C0 setne %al
50 001f 84C0 testb %al, %al
51 0021 7414 je .L4
52 0023 BF000000 movl $_ZZ15static_var_funcvE1a, %edi
52 00
53 0028 E8000000 call _ZN1AC1Ev
53 00
54 002d BF000000 movl $_ZGVZ15static_var_funcvE1a, %edi
54 00
55 0032 E8000000 call __cxa_guard_release
55 00
56 .L4:
13:yy.cpp ****
return
a.a++;
57 .loc 1 13 0
58 0037 8B050000 movl _ZZ15static_var_funcvE1a(%rip), %eax
58 0000
59 003d 89C2 movl %eax, %edx
60 003f 83C001 addl $1, %eax
61 0042 89050000 movl %eax, _ZZ15static_var_funcvE1a(%rip)
61 0000
62 0048 89D0 movl %edx, %eax
63 .LBE2:
14:yy.cpp **** }
64 .loc 1 14 0
65 004a C9 leave
66 004b C3 ret
|
亮点就在__cxa_guard_acquire和__cxa_guard_release上,这两个函数实现于libstdc++。大意是一个全局的mutex和一个cond来保护一个锁变量(_ZGVZ15static_var_funcvE1a),锁变量再来保护目标变量(_ZZ15static_var_funcvE1a)。锁变量的第一个字节(也就是%al)表示目标变量是否被初始化过了,第二个字节表示目标变量是否在初始化中。__cxa_guard_acquire的时候将锁变量的第二个字节置1,表示初始化中;如果已经为1了,就等待它变0(通过全局cond)再返回(保证初始化结束)。__cxa_guard_release的时候清除第二个字节,再将第一个字节置1。
简单地说,g++在变量初始化的前后,自动加了锁保护代码。
另外还有一种可能的情况,变量初始化的时候,如果存在自引用,可能会循环初始化产生死锁。g++对这种情况也有考虑。
http://www.dutor.net/index.php/2013/06/initialization-of-local-static-variables/
不必说静态变量和普通变量的区别,也不必说静态变量及其作用域的得与失,单单说一下函数作用域的静态变量是如何初始化的。
1 2 3 4 5 6 | |
在 foo() 第一次被调用时,foo()::s 只初始化一次(C 中,静态变量只允许以常量初始化)。“只初始化一次”是如何保证的呢?当然需要编译器维护一个状态,来标识该变量是否已被初始化,并安插代码,在每一次函数被调用时进行判断。咱们通过汇编验证一把:
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 | .globl _Z3foov .type _Z3foov, @function _Z3foov: .LFB1405: pushq %rbp .LCFI13: movq %rsp, %rbp .LCFI14: movl $_ZGVZ3foovE1n, %eax movzbl (%rax), %eax testb %al, %al jne .L18 movl $_ZGVZ3foovE1n, %edi call __cxa_guard_acquire testl %eax, %eax setne %al testb %al, %al je .L18 call _Z4initv movl %eax, _ZZ3foovE1n(%rip) movl $_ZGVZ3foovE1n, %edi call __cxa_guard_release .L18: movl _ZZ3foovE1n(%rip), %eax leave ret |
寄存器、指令和标号不提,其他符号是什么含义呢?通过 c++filt 进行 demangling,_ZGVZ3foovE1n 标识 ‘guard variable for foo()::n’,作为前面提到的“初始化状态标识”用(低字节),_ZZ3foovE1n 标识 ‘foo()::n’,_Z4initv 即 init()。
那 __cxa_guard_acquire 和 __cxa_guard_release 呢?故名思议,这两个函数具有锁语义。为什么需要锁呢?当然是基于静态变量的线程安全考虑了。静态变量的状态变化属于业务逻辑,编译器管不着也管不了,但静态变量的初始化过程由编译器负责,在初始化线程安全的问题上还是可以出把力的。
分析上述汇编代码。首先获取 guard 变量,判断低字节是否为 0,若非零,表示已经初始化,可以直接使用。否则,将 guard 作为参数调用 __cxa_guard_acquire,如果锁成功,调用 init() 初始化静态变量 foo()::n,然后释放锁。如果锁失败,说明产生竞态条件,则会阻塞当前线程,不同于普通锁的地方在于,__cxa_guard_acquire 是有返回值的(当然 pthread_lock 也有返回值,但用途不同),如果发生了等待,__cxa_guard_acquire 返回 0,并不会进入 foo()::n 的初始化过程(其他线程已经初始化过了,初始化失败的情况就不细究了)。
为了验证上述分析,可以将 init() 实现成一个耗时的操作,令多个线程“同时”调用 foo(),然后查看各个线程的运行状态。
利用该机制,可以很好的实现所谓 Singleton 模式:
1 2 3 4 5 | |
对于单线程程序,静态变量的保护是没有必要的,g++ 的 -fno-threadsafe-statics 选项可以禁掉该机制。
由于博主注明未经允许不能转载,所以这里只贴上结论,有兴趣的同学可去原链接查看。
局部static变量test在函数f()内定义。但是C++规定,局部static变量只会初始化一次!所以第一个程序中后面调用两次f()都不会初始化test。而第二个程序并不是初始化,而是赋值! static int test =cnt与 static int test; test = cnt是不等价的!!
四、下面这篇转自 http://blog.csdn.net/wangxiaokang1_1/article/details/11020409 实际原文地址不确定。这一篇是有关”另外为什么静态变量只会被初始化一次呢:”,不是从汇编角度,而是从实际程序效果反推的。
C++ 怎么让静态变量只初始化一次
童鞋们在学习C++的时候,往往只是按照书本上的原文去强行记忆各种特性,比方说,静态变量只初始化一次。你心中一定在默念:一定要记住,static只会初始化一次云云,希望自己能够记住。告诉你,你为什么总是记不住,因为你没有正真理解静态变量的原理, 所以下面我就来告诉大家它的原理,直接上代码:
[code=C/C++]
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
int initNum = 3;
for (int i=5; i > 0; --i)
{
static int n1 = initNum;
n1++;
printf("%d\n", n1);
}
getchar();
return 0;
}
[/code]
输出结果:
4
5
6
7
8
在这里我们可以看到虽然代码循环了5次,静态变量n1确实只初始化了一次。那么为什么呢?继续上代码,相信大家就会明白些许了。
[code=C/C++]
int _tmain(int argc, _TCHAR* argv[])
{
int initNum = 3;
for (int i=5; i > 0; --i)
{
static int n1 = initNum;
//我们在这里了两句代码
int* p = &n1;
p++;
*p = 0;
//end
n1++;
printf("%d\n", n1);
}
getchar();
return 0;
}
[/code]
输出结果:
4
4
4
4
4
这次,静态变量居然跟随着5次循环也初始化了5次。你一定非常诧异,其实我们不难推断,其实静态变量就是通过静态变量后面的一个32位内存位来做记录,以标识这个静态变量是否已经初始化。而我们的p++;*p = 0;却每次都将这个值赋值为0,所以程序就一直认为n1一直没有被初始化过,并每次都初始化一次。看一下内存,就更明了了:
0x00E8716C 03 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 b0 e7 1e 6a 00 00 00
这里的内存地址就是静态变量n1的地址,值是3,后面还有一个1,你看到了吗,这个就是程序用来记录该静态变量是否初始化的标识位啦。现在你一定明白原理了,并且能轻松记住静态变量的特性了吧?
童鞋们还可以试一下,多个静态变量时,标识位的表示形式,以深入学习(透露一下,每一位标识一个静态变量的初始化状态)。
以上代码有一点需要说明:代码中之所以要用int initNum = 3;而不是直接用static int n1 = 3;是因为如果给静态变量直接赋值一个常量的话,编译器会进行优化,导致程序在一启动时,就初始化好了,不便于我们观察静态变量内存上的改变。
(转载完)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。