从汇编看c++中的static关键字

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++中全局对象和全局变量》)一样。

原文地址:https://www.cnblogs.com/chaoguo1234/p/3061420.html