c++中的static关键字可以修饰全局变量,局部变量和类成员数据(当然还有类的成员函数,但是这里只讨论static修饰变量的情况)。对于static修饰全局变量的情况,和单纯的全局变量类似,生命期存在于整个程序执行期间,在程序加载后,第一条程序语句执行之前就已存在,只是编译器限制它只有文件作用域(即只能在本文件访问)。因此,static修饰的全局变量等价于只有文件作用域的全局变量。
对于static修饰的局部变量有点特殊,该变量的可见性(也就是作用域)任是函数里面,但是生命期确实整个程序运行期间,下面来看一看实际情形:
c++源码:
void add(int i) { static int sum = 0; sum += i; } int main() { add(5); }
对应的汇编码:
_BSS SEGMENT ?sum@?1??add@@YAXH@Z@4HA DD 01H DUP (?) ; sum的内存空间 ; Function compile flags: /Odtp _BSS ENDS _TEXT SEGMENT _i$ = 8 ; size = 4 ?add@@YAXH@Z PROC ; add ; 1 : void add(int i) { push ebp mov ebp, esp ; 2 : static int sum = 0; ; 3 : sum += i; mov eax, DWORD PTR ?sum@?1??add@@YAXH@Z@4HA;取sum的值放入寄存器eax add eax, DWORD PTR _i$[ebp];取参数i的值,与eax中的值相加 mov DWORD PTR ?sum@?1??add@@YAXH@Z@4HA, eax;将相加的结果放入sum对应的存储空间 ; 4 : } pop ebp ret 0 ?add@@YAXH@Z ENDP ; add _TEXT ENDS PUBLIC _main ; Function compile flags: /Odtp _TEXT SEGMENT _main PROC ; 6 : int main() { push ebp mov ebp, esp ; 7 : add(5); push 5;将参数5压栈 call ?add@@YAXH@Z ; 调用add函数 add esp, 4 ; 8 : } xor eax, eax pop ebp ret 0 _main ENDP _TEXT ENDS
可以看到,局部静态变量sum不是向普通局部变量一样被分配在栈空间上,而是被分配到了内存中的静态数据区(由_BBS指定),因此它的声明期不在受栈空间的分配和释放影响,而是整个程序的运行期间,但是他的可见性(作用域)任然只存于函数内,这是由编译器来保证的。(通过名称粉碎法实现)
然而,局部静态变量又是如何实现只初始化一次呢,请看下面的代码:
C++源码:
void add(int i) { static int sum = i; sum++; } int main() { for (int i = 0; i < 3; i++) { add(i); } }
由于main函数里面的汇编只实现了循环的控制和add函数的调用,所以我们重点看add函数的汇编码:
_TEXT SEGMENT _i$ = 8 ; 参数i的偏移地址 ?add@@YAXH@Z PROC ; add函数定义 ; 1 : void add(int i) { push ebp mov ebp, esp ; 2 : static int sum = i; mov eax, DWORD PTR ?$S1@?1??add@@YAXH@Z@4IA;取地址?$S1@?1??add@@YAXH@Z@4IA中的值(初值为0)到eax寄存器中,这个地址里面的值作为sum是否被初始化过的标志 and eax, 1;//相与之后最低位只能是0或者1 jne SHORT $LN1@add;//如果上面语句相与不为0,就跳转到$LN1@add处执行,说明sum已经初始化了 mov ecx, DWORD PTR ?$S1@?1??add@@YAXH@Z@4IA;//如果相与为0就执行这一句(说明sum还没初始化),将?$S1@?1??add@@YAXH@Z@4IA地址里面的值存入ecx or ecx, 1;相或之后ecx的最低位一定为1 mov DWORD PTR ?$S1@?1??add@@YAXH@Z@4IA, ecx;将ecx的值写入地址?$S1@?1??add@@YAXH@Z@4IA里面 mov edx, DWORD PTR _i$[ebp];获取参数i的值 mov DWORD PTR ?sum@?1??add@@YAXH@Z@4HA, edx;将参数i的值写个sum $LN1@add: ; 3 : sum++; mov eax, DWORD PTR ?sum@?1??add@@YAXH@Z@4HA;获取sum的值,存入寄存器eax add eax, 1;执行加1操作 mov DWORD PTR ?sum@?1??add@@YAXH@Z@4HA, eax;将结果写回sum ; 4 : } pop ebp ret 0
从上面的汇编码可以看到,保证局部静态变量只初始化一次的原因是对地址?$S1@?1??add@@YAXH@Z@4IA里面的值的最低位的判断,如果最低位为0,说明还没初始化,就先执行初始化操作,再执行加1操作;如果最低位为1,说明已经初始化,就跳过初始化操作,直接执行加1操作。
当局部静态变量初始化值为一个常量的时候,又有不同:
c++源码:
void add(int i) { static int sum = 2;//初始化值为常量 sum++; } int main() { for (int i = 0; i < 3; i++) { add(i); } }
下main是add函数的汇编代码:
_DATA SEGMENT ?sum@?1??add@@YAXH@Z@4HA DD 02H ; 局部静态变量sum的内存空间,在分配内存的时候已经写入初始值 ; Function compile flags: /Odtp _DATA ENDS _TEXT SEGMENT _i$ = 8 ; size = 4 ?add@@YAXH@Z PROC ; add ; 1 : void add(int i) { push ebp mov ebp, esp ; 2 : static int sum = 2; ; 3 : sum++; mov eax, DWORD PTR ?sum@?1??add@@YAXH@Z@4HA;//取sum的值 add eax, 1;加1操作 mov DWORD PTR ?sum@?1??add@@YAXH@Z@4HA, eax;写回sum的值 ; 4 : } pop ebp ret 0
从上面可以看到,add函数里面已经没有了像上面的判断sum是否初始化的语句。这是因为由于初始值是常量,编译器做了优化,因为对于一个常量,即使初始化多次,值也不会改变,因此没有判断的必要。
对于static修饰的类的成员数据,原理和静态全局变量一样,生命期为整个程序运行期间,在程序加载之后,第一条程序语句执行之前就已存在,只是有一个特殊的作用域,即这个变量从属于类。
局部静态对象和全局静态对象
局部静态对象何时被构造,又何时被析构,先看c++源码:
#include <iostream> using namespace std; class X { public: int i; public: X(int ii = 0) : i(ii) { } ~X() {} }; X getX() { static X x; return x; } int main() { X x = getX(); }
代码在函数getX里面定义了一个局部静态对象,下面来看其构造时的汇编码:
static X x; 00FA13E7 mov eax,dword ptr [$S1 (0FA9138h)] ;和局部静态变量一样,作为标志 00FA13EC and eax,1 00FA13EF jne getX+85h (0FA1425h) ;跳转到地址0FA1425h处执行 00FA13F1 mov eax,dword ptr [$S1 (0FA9138h)] 00FA13F6 or eax,1 00FA13F9 mov dword ptr [$S1 (0FA9138h)],eax 00FA13FE mov dword ptr [ebp-4],0 00FA1405 push 0 ;压入参数0,传给构造函数 00FA1407 mov ecx,offset x (0FA913Ch) ;取对象x的首地址给寄存器ecx,作为隐含参数传递给构造函数 00FA140C call X::X (0FA1046h) ;调用构造函数 00FA1411 push offset `getX'::`2'::`dynamic atexit destructor for 'x'' (0FA5610h) ;通过atexit函数注册析构代理函数 00FA1416 call @ILT+100(_atexit) (0FA1069h) 00FA141B add esp,4 00FA141E mov dword ptr [ebp-4],0FFFFFFFFh 15: return x; 00FA1425 mov eax,dword ptr [ebp+8] 00FA1428 mov ecx,dword ptr [x (0FA913Ch)] 00FA142E mov dword ptr [eax],ecx 00FA1430 mov edx,dword ptr [ebp-0D4h] 00FA1436 or edx,1 00FA1439 mov dword ptr [ebp-0D4h],edx 00FA143F mov eax,dword ptr [ebp+8] 16: }
从汇编码可以看到,和局部静态变量一样,构造局部静态对象时也有判断标志,并且还通过atexit函数注册了析构代理函数。当程序退出时,便会调用此注册的析构代理函数,析构代理函数再调用真正的对象x的析构函数。
至于静态全局对象,除了作用域限制于本文件之外,其他都和全局对象(《从汇编看c++中全局对象和全局变量》)一样。