C++函数中静态变量的生命周期是多久?

418

如果在函数的作用域中声明一个变量为static,它只会被初始化一次,并保留其值直到函数调用。那么它的生命周期是什么时候?它的构造函数和析构函数何时被调用?

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

301

static变量的生命周期始于程序流第一次遇到它的声明,结束于程序终止。这意味着运行时必须进行一些书记记录,以便仅在实际构造时才进行析构。

此外,由于标准规定静态对象的析构函数必须按照其构造完成的相反顺序运行,并且构造顺序可能取决于特定的程序运行,因此必须考虑构造顺序。

示例

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++983.6.3.1[basic.start.term]

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


3
对于没有构造/析构函数副作用的简单类型,将它们初始化方式与全局简单类型相同是一种直接的优化方法。这避免了分支、标志和销毁顺序等问题。这并不意味着它们的生命周期有任何不同。 - John McFarlane
1
这里不适用于全局对象的“析构函数必须按照它们构造完成的相反顺序运行”的规则,因为这些对象不是全局的。具有静态或线程存储期限的本地变量的销毁顺序比纯LIFO复杂得多,请参见第3.6.3节**[basic.start.term]**。 - Ben Voigt
2
“在程序终止时”这个短语并不严格正确。那么在 Windows dll 中动态加载和卸载的静态内容呢?显然,C++ 标准根本不涉及程序集(如果它能涉及就好了),但是对于标准在这里确切表述的澄清会很有帮助。如果包括“在程序终止时”这个短语,那么任何具有动态卸载程序集功能的 C++ 实现都将不符合规范。 - Roger Sanders
1
@RogerSanders 这是一个很好的观点,我使用的标准(C++11)指出具有静态存储期的对象的析构函数会在从 main 返回和调用 std::exit 的结果中被调用。您能否指出标准中明确允许动态库的部分? - Motti
2
@Motti 我不相信标准明确允许动态库,但是直到现在我也不相信标准中有什么特别与其实现不一致的东西。当然,严格来说,这里的语言并没有说明静态对象不能通过其他方式更早地被销毁,只是必须在返回主函数或调用std::exit时销毁它们。我认为这是一条非常微妙的界线。 - Roger Sanders
显示剩余4条评论

134

Motti的关于顺序的说法是正确的,但还有其他一些需要考虑的问题:

编译器通常使用隐藏的标志变量来指示本地静态变量是否已经初始化,而这个标志在每次进入函数时被检查。显然,这会带来一些性能损失,但更令人担忧的是,这个标志不能保证线程安全。

如果你有一个像上面那样的本地静态变量,并且foo从多个线程中调用,你可能会遇到竞争条件,导致plonk被错误地初始化或甚至多次初始化。此外,在这种情况下,plonk可能会被一个不同的线程析构,而非构造它的线程。

尽管标准规定了顺序,但我仍然非常谨慎地对待本地静态销毁的实际顺序,因为你可能无意中依赖于静态对象在被销毁后仍然有效,这真的很难追踪。


77
C++0x 要求静态初始化是线程安全的。所以请谨慎,但事情只会变得更好。 - deft_code
破坏顺序问题可以通过一些策略避免。静态/全局对象(单例等)不应在其方法体中访问其他静态对象。它们只能在构造函数中访问,在那里可以存储引用/指针以供后续方法访问。这并不完美,但应该解决99%的情况,而它无法捕获的情况显然是可疑的,应在代码审查中捕获。这仍然不是完美的解决方案,因为该策略无法在语言中强制执行。 - deft_code
我有点新手,但为什么不能在语言中执行此策略? - cjcurrie
10
自C++11起,这个问题不再存在了。Motti的答案已经根据这一点进行了更新。 - Nilanjan Basu

10

如果没有标准中实际规则的真实规定,那么现有的解释并不完整,该规定在 6.7 中找到:

所有具有静态存储期或线程存储期的块作用域变量的零初始化是在任何其他初始化之前执行的。如适用,则在首次进入其块之前执行带有静态存储期的块作用域实体的常量初始化。 在相同条件下,实现允许对具有静态或线程存储期的其他块作用域变量进行早期初始化,就像在名称空间范围内静态初始化具有静态或线程存储期的变量一样。否则,这种变量将在控制第一次通过其声明时进行初始化; 这种变量被认为在其初始化完成时已经初始化。如果初始化通过抛出异常退出,则初始化尚未完成,因此下次控制进入声明时会再次尝试。 如果在初始化变量时同时并发地进入声明,则并发执行必须等待初始化完成。 如果在初始化变量时递归地重新进入声明,则行为是未定义的。


8

就我所知,Codegear C++Builder不按照标准预期的顺序进行析构。

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

这也是不依赖销毁顺序的另一个原因!


57
不是一个好的论点。我认为这更像是一个不使用这个编译器的论据。 - Martin York
27
如果你有兴趣编写真正可移植的代码,而不仅仅是理论上的可移植代码,那么了解语言中可能会出现问题的领域是很有用的。我会感到惊讶如果C++Builder在这方面是独一无二的。 - Roddy
18
我同意,只是我会把它表述为“哪些编译器会导致问题,以及它们会在语言的哪些方面出现问题”;-P - Steve Jessop

0

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

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


1
这并不适用于函数作用域中的变量。 - awerries

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接