【问题标题】:how do static variables inside functions work?函数内部的静态变量如何工作?
【发布时间】:2011-10-12 13:23:15
【问题描述】:

在以下代码中:

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

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

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

编译器如何处理这个问题?

【问题讨论】:

  • 尝试使用具有非平凡构造函数的类型。您应该看到更多关于实际操作的痕迹。 (请参阅 stackoverflow.com/questions/6967179/… 以了解使用 GCC 的这种类型的静态初始化程序会发生什么类型的事情)
  • @Mat: 非平凡的非 constexpr,有人可能会补充。例如。 std::shared_ptr() 可以。

标签: c++ c compiler-construction static


【解决方案1】:

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

您在生成的程序集中没有看到任何检查的原因是,由于 n 是一个 int 变量,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 注释标记的行是条件跳转指令,如果变量已经指向有效对象,则跳过构造。

【讨论】:

  • 好点 - 如果变量被初始化为常量表达式,那么您可能根本不需要任何标志。
【解决方案2】:

这完全取决于实现;语言标准对此只字未提。

实际上,编译器通常会在某处包含一个隐藏标志变量,以指示静态变量是否已被实例化。静态变量和标志可能会在程序的静态存储区域(例如数据段,而不是堆栈段)中,而不是在函数范围内存中,因此您可能需要在程序集中四处查看。 (变量不能在调用栈上,原因很明显,所以它真的很像一个全局变量。“静态分配”真的涵盖了各种静态变量!)

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

【讨论】:

  • 我从未听说过这些秘密的“隐藏标志”。哪个编译器可以做到这一点,你能提供任何来源吗?至少我见过的每个编译器/链接器都只是在程序启动时初始化所有静态变量,然后像使用任何全局变量一样威胁它们。编辑:啊没关系,没有看到 C++ 标签。您可能应该清楚,此“标志”仅适用于类构造函数,而不适用于 int 等原始数据类型。
  • @Lundin:void foo() { static int n = read_from_user(); } 呢?如何在加载时对其进行初始化?
  • 我很确定它也适用于 C 如果你初始化为例如函数static int foo = get_initial_foo();的结果
  • @Random:在 C 中,初始化元素必须是常量。
  • 嗯,实际上标准对这些事情的行为方式非常具体,包括运行非平凡构造函数和析构函数的顺序。
【解决方案3】:

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

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

【讨论】:

  • 函数静态变量在第一次调用时初始化,而不是在启动时。
  • @R.MartinhoFernandes 应用 as-if 规则,所有编译器都会将值 5 放入二进制文件中,并且在第一次调用时不执行任何代码。
  • @DavidHeffernan 哦,我没有太注意这个例子。你是对的。
  • 是的,这正是它的做法。所有静态变量,无论它们在哪里声明,以及所有全局变量,都将被放置在一个特殊的 RAM 段中,该段在程序启动时被初始化。当执行带有“本地”静态的代码时,它被视为静态在函数外部声明并已初始化。
  • 需要注意的是,虽然 C++ 支持由非常量表达式初始化的静态局部变量,但 C 似乎不允许这样做,至少 gcc 在你说 static int x = rand() 时会说 error: initializer element is not constant。跨度>
猜你喜欢
  • 2016-07-26
  • 2011-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-13
  • 1970-01-01
  • 2015-09-29
  • 2013-11-04
相关资源
最近更新 更多