【问题标题】:What is the lifetime of a static variable in a C++ function?C++ 函数中静态变量的生命周期是多少?
【发布时间】:2010-09-19 18:44:44
【问题描述】:

如果一个变量在函数的作用域中被声明为static,它只会被初始化一次并且在函数调用之间保持它的值。它的寿命究竟是多少?什么时候调用它的构造函数和析构函数?

void foo() 
{ 
    static string plonk = "When will I die?";
}

【问题讨论】:

    标签: c++ static lifetime


    【解决方案1】:

    函数static 变量的生命周期从程序流第一次遇到声明时开始[0],并在程序终止时结束。这意味着运行时必须执行一些记录,以便只有在实际构建时才将其销毁。

    另外,由于标准规定静态对象的析构函数必须按照其构造完成的相反顺序运行[1],而构造的顺序可能取决于具体的程序运行,必须考虑到构造顺序。

    示例

    struct emitter {
        string str;
        emitter(const string& s) : str(s) { cout << "Created " << str << endl; }
        ~emitter() { cout << "Destroyed " << str << endl; }
    };
    
    void foo(bool skip_first) 
    {
        if (!skip_first)
            static emitter a("in if");
        static emitter b("in foo");
    }
    
    int main(int argc, char*[])
    {
        foo(argc != 2);
        if (argc == 3)
            foo(false);
    }
    

    输出:

    C:>sample.exe
    创建于 foo
    在 foo 中销毁

    C:>sample.exe 1
    创建于 if
    创建于 foo
    在 foo 中销毁
    在 if 中销毁

    C:>sample.exe 1 2
    创建于 foo
    创建于 if
    毁于 if
    在 foo 中销毁

    [0] 由于 C++98[2] 没有提及多线程,因此未指定多线程环境中的行为方式,并且可以像Roddy 提到的那样有问题。

    [1] C++98 部分3.6.3.1 [basic.start.term]

    [2] 在 C++11 中静态以线程安全的方式初始化,这也称为Magic Statics

    【讨论】:

    • 对于没有 c'tor/d'tor 副作用的简单类型,以与全局简单类型相同的方式初始化它们是一种直接的优化。这避免了分支、标志和破坏顺序问题。这并不是说他们的一生有什么不同。
    • 如果函数可以被多个线程调用,那么这是否意味着在C++98中需要确保静态声明必须被互斥体保护??
    • “全局对象的析构函数必须按照它们完成构造的相反顺序运行”在这里不适用,因为这些对象不是全局的。具有静态或线程存储持续时间的局部变量的破坏顺序比纯 LIFO 复杂得多,请参阅第 3.6.3 节 [basic.start.term]
    • “在程序终止时”这句话并不完全正确。动态加载和卸载的 Windows dll 中的静态变量怎么样?显然,C++ 标准根本不处理程序集(如果这样做就好了),但是澄清标准在这里所说的确切内容会很好。如果包含“在程序终止时”这一短语,从技术上讲,它会使任何具有动态卸载程序集的 C++ 实现不符合标准。
    • @Motti 我不相信该标准确实明确允许动态库,但直到现在我也不相信该标准中有任何具体内容与其实施不一致。当然,严格来说这里的语言并没有说明不能通过其他方式提前销毁静态对象,只是在从 main 返回或调用 std::exit 时必须销毁它们。虽然我认为这是一条非常好的路线。
    【解决方案2】:

    Motti 对订单的看法是正确的,但还有其他一些事情需要考虑:

    编译器通常使用隐藏标志变量来指示本地静态变量是否已经初始化,并且在函数的每个条目上都会检查此标志。显然这是一个小的性能损失,但更令人担忧的是这个标志不能保证是线程安全的。

    如果您有上述本地静态,并且从多个线程调用 foo,您可能会遇到竞争条件导致 plonk 被错误地初始化甚至多次初始化。此外,在这种情况下,plonk 可能会被与构造它的线程不同的线程破坏。

    尽管标准说了什么,我还是会非常警惕局部静态破坏的实际顺序,因为您可能会在不知不觉中依赖静态在它被破坏后仍然有效,这真的很难追踪下。

    【讨论】:

    • C++0x 要求静态初始化是线程安全的。所以要小心,但事情只会变得更好。
    • 通过一些策略可以避免破坏顺序问题。静态/全局对象(单例等)不应访问其方法体中的其他静态对象。它们只能在可以存储引用/指针以供以后在方法中访问的构造函数中访问。这并不完美,但应该修复 99 个案例,它没有捕获的案例显然是可疑的,应该在代码审查中发现。这仍然不是一个完美的解决方案,因为该政策无法在该语言中执行
    • 我有点菜鸟,但为什么不能在语言中执行此政策?
    • 从 C++11 开始,这不再是问题。 Motti 的答案就是据此更新的。
    【解决方案3】:

    如果没有 6.7 中的标准中的实际规则,现有的解释并不完整:

    所有具有静态存储持续时间或线程存储持续时间的块范围变量的零初始化在任何其他初始化发生之前执行。具有静态存储持续时间的块范围实体的持续初始化(如果适用)在首次进入其块之前执行。在允许实现在命名空间范围内静态初始化具有静态或线程存储持续时间的变量的相同条件下,允许实现对具有静态或线程存储持续时间的其他块范围变量执行早期初始化。否则,此类变量在控件第一次通过其声明时被初始化;这样的变量在其初始化完成时被认为已初始化。如果初始化通过抛出异常退出,则初始化 不完整,所以下次控制进入声明时会再次尝试。如果在初始化变量时控制同时进入声明,则并发执行将等待初始化完成。如果在初始化变量时控件以递归方式重新进入声明,则行为未定义。

    【讨论】:

      【解决方案4】:

      FWIW,Codegear C++Builder 不会按照标准按预期顺序进行破坏。

      C:\> sample.exe 1 2
      Created in foo
      Created in if
      Destroyed in foo
      Destroyed in if
      

      ...这也是不依赖销毁命令的另一个原因!

      【讨论】:

      • 不是一个好的论据。我会说这更像是一个不使用这个编译器的论点。
      • 嗯。如果您对生成真实世界的可移植代码感兴趣,而不仅仅是理论上的可移植代码,我认为了解该语言的哪些区域会导致问题很有用。如果 C++Builder 在不处理这个方面是独一无二的,我会感到惊讶。
      • 我同意,但我将其表述为“哪些编译器会导致问题,以及它们使用的语言的哪些领域”;-P
      【解决方案5】:

      静态变量程序执行开始后开始发挥作用,并且在程序执行结束之前一直可用。

      静态变量在内存的数据段中创建。

      【讨论】:

      • 这不适用于函数范围内的变量
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-12
      • 2013-07-27
      相关资源
      最近更新 更多