zoukankan      html  css  js  c++  java
  • 从汇编看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++中全局对象和全局变量》)一样。

  • 相关阅读:
    Git之将master合并到自己分支
    React 中的不可变数据 — Immer
    数据治理项目
    每日Excel系列
    Python实战网站开发:Day6编写配置文件
    Python实战网站开发:Day5搭建Web框架
    Python实战网站开发:Day7搭建MVC
    Visual Studio Code调试模式,出现无法打开,文件是目录错误的解决办法
    第1章 Vue.js2.0由浅入深:基本用法
    Python实战网站开发:Day4编写Model
  • 原文地址:https://www.cnblogs.com/chaoguo1234/p/3061420.html
Copyright © 2011-2022 走看看