函数内部的静态变量是如何工作的

how do static variables inside functions work?

本文关键字:工作 何工作 内部 静态 变量 函数      更新时间:2023-10-16

在以下代码中:

int count(){
    static int n(5);
    n = n + 1;
    return n;
}

变量n只在第一次调用函数时实例化一次。

应该有一个标志之类的东西,这样它只初始化变量一次。我试图查看从gcc生成的汇编代码,但没有任何线索。

编译器如何处理这个?

当然,这是特定于编译器的。

您在生成的程序集中没有看到任何检查的原因是,由于nint变量,g++只是将其视为预初始化为5的全局变量。

让我们看看如果我们对std::string:

做同样的事情会发生什么
#include <string>
void count() {
    static std::string str;
    str += ' ';
}
生成的程序集如下所示:
_Z5countv:
.LFB544:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        .cfi_lsda 0x3,.LLSDA544
        pushq   %rbp
        .cfi_def_cfa_offset 16
        movq    %rsp, %rbp
        .cfi_offset 6, -16
        .cfi_def_cfa_register 6
        pushq   %r13
        pushq   %r12
        pushq   %rbx
        subq    $8, %rsp
        movl    $_ZGVZ5countvE3str, %eax
        movzbl  (%rax), %eax
        testb   %al, %al
        jne     .L2                     ; <======= bypass initialization
        .cfi_offset 3, -40
        .cfi_offset 12, -32
        .cfi_offset 13, -24
        movl    $_ZGVZ5countvE3str, %edi
        call    __cxa_guard_acquire     ; acquire the lock
        testl   %eax, %eax
        setne   %al
        testb   %al, %al
        je      .L2                     ; check again
        movl    $0, %ebx
        movl    $_ZZ5countvE3str, %edi
.LEHB0:
        call    _ZNSsC1Ev               ; call the constructor
.LEHE0:
        movl    $_ZGVZ5countvE3str, %edi
        call    __cxa_guard_release     ; release the lock
        movl    $_ZNSsD1Ev, %eax
        movl    $__dso_handle, %edx
        movl    $_ZZ5countvE3str, %esi
        movq    %rax, %rdi
        call    __cxa_atexit            ; schedule the destructor to be called at exit
        jmp     .L2
.L7:
.L3:
        movl    %edx, %r12d
        movq    %rax, %r13
        testb   %bl, %bl
        jne     .L5
.L4:
        movl    $_ZGVZ5countvE3str, %edi
        call    __cxa_guard_abort
.L5:
        movq    %r13, %rax
        movslq  %r12d,%rdx
        movq    %rax, %rdi
.LEHB1:
        call    _Unwind_Resume
.L2:
        movl    $32, %esi
        movl    $_ZZ5countvE3str, %edi
        call    _ZNSspLEc
.LEHE1:
        addq    $8, %rsp
        popq    %rbx
        popq    %r12
        popq    %r13
        leave
        ret
        .cfi_endproc

我用bypass initialization注释标记的行是条件跳转指令,如果变量已经指向有效对象,则跳过构造。

这完全取决于实现;语言标准对此没有规定。

实际上,编译器通常会在某个地方包含一个隐藏的标志变量,用于指示静态变量是否已经实例化。静态变量和标志可能在程序的静态存储区域(例如数据段,而不是堆栈段),而不是在函数作用域内存中,所以你可能需要在程序集中查找。(由于显而易见的原因,该变量不能进入调用堆栈,因此它实际上就像一个全局变量。"静态分配"真的涵盖了所有类型的静态变量!)

Update:正如@aix所指出的,如果静态变量初始化为常量表达式,您甚至可能不需要标记,因为初始化可以在加载时执行,而不是在第一次函数调用时执行。在c++ 11中,由于常量表达式的广泛可用性,您应该能够比c++ 03更好地利用这一点。

这个变量很可能会被gcc当作普通的全局变量来处理。这意味着初始化将在二进制文件中直接静态初始化。

这是可能的,因为你用一个常量初始化它。如果你初始化它。对于另一个函数返回值,编译器将添加一个标志,并跳过基于该标志的初始化。