C ++函数中静态变量的生存期是多少?


373

如果将变量声明为static在函数范围内,则该变量仅初始化一次,并在函数调用之间保留其值。它的寿命到底是什么?何时调用其构造函数和析构函数?

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

Answers:


257

函数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中
销毁

C:> SAMPLE.EXE 1 2
创建在FOO
创建于如果
毁于如果
销毁在FOO

[0]由于C ++ 98 [2]没有引用多线程,因此在多线程环境中如何表现尚无定论,如Roddy所述,这可能是有问题的。

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

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


2
对于没有c'tor / d'tor副作用的简单类型,以与全局简单类型相同的方式初始化它们是一种直接的优化。这避免了分支,标志和破坏顺序的问题。这并不是说他们的一生都不同。
约翰·麦克法兰

1
如果该函数可以被多个线程调用,那么这是否意味着您需要确保C ++ 98中的静态声明必须由互斥体保护?
allyourcode 2014年

1
“全局对象的析构函数必须按照其构造完成的相反顺序运行”在这里不适用,因为这些对象不是全局的。具有静态或线程存储持续时间的本地对象的销毁顺序比纯LIFO复杂得多,请参阅第3.6.3节[basic.start.term]
Ben Voigt 2015年

2
短语“在程序终止时”并非严格正确。动态加载和卸载的Windows dll中的静态变量如何处理?显然,C ++标准根本不涉及程序集(如果可以的话)会很不错,但是明确说明该标准在这里所说的内容会很好。如果包括短语“程序终止时”,则从技术上讲,它将使具有动态卸载的程序集的C ++的任何实现都不合格。
罗杰·桑德斯

2
@Motti我不认为该标准确实允许动态库,但是直到现在我也一直不相信该标准中有任何与实现不符的内容。当然,严格来说,这里的语言并没有说明静态对象不能通过其他方式更早地销毁,只是在从main返回或调用std :: exit时必须销毁它们。虽然我认为这是一条很好的路线。
罗杰·桑德斯

125

Motti关于顺序是正确的,但还需要考虑其他一些事项:

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

如果您具有如上所述的局部静态变量,并且foo从多个线程中调用了该静态变量,则您可能存在竞争条件,导致plonk错误地初始化了它甚至多次。同样,在这种情况下plonk,线程可能会被与构造它的线程不同的线程破坏。

尽管有标准说明,但我会对本地静态销毁的实际顺序非常警惕,因为您可能会无意间依赖静态销毁后仍然有效的静态数据,这确实很难追踪。


68
C ++ 0x要求静态初始化必须是线程安全的。所以要当心,但是事情只会变得更好。
deft_code 2010年

只需少量策略即可避免销毁订单问题。静态/全局对象(子集等)不得访问其方法主体中的其他静态对象。它们只能在可以存储引用/指针以供以后在方法中访问的构造函数中访问。这不是完美的方法,但是应该修复99个案例,而没有发现的案例显然是可疑的,应该在代码审查中发现。这仍然不是一个完美的解决方案,因为无法以该语言执行该政策
deft_code 2010年

我有点菜鸟,但为什么不能用该语言强制执行此策略?
cjcurrie 2013年

9
从C ++ 11开始,这不再是问题。Motti的答案会据此进行更新。
Nilanjan Basu

10

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

在进行任何其他初始化之前,将使用静态存储持续时间或线程存储持续时间对所有块范围变量进行零初始化。如果适用,将在首次进入其块之前执行具有静态存储持续时间的块范围实体的恒定初始化。在允许实现以静态或线程存储持续时间在命名空间范围内静态初始化变量的相同条件下,允许实现以静态或线程存储持续时间对其他块范围变量进行早期初始化。否则,该变量将在控件第一次通过其声明时进行初始化;此类变量在初始化完成后即被初始化。如果初始化由于抛出异常而退出,初始化未完成,因此下次控件进入声明时将再次尝试。如果在初始化变量时控件同时输入声明,则并发执行应等待初始化完成。如果控件在初始化变量时递归地重新输入声明,则该行为未定义。


8

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

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

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


57
这不是一个很好的论点。我会说这更多是一个不使用此编译器的参数。
马丁·约克

26
嗯 如果您对产生实际的可移植代码感兴趣,而不仅仅是对理论上可移植的代码感兴趣,那么我认为了解语言的哪些方面会引起问题很有用。如果C ++ Builder在处理此问题方面是独一无二的,我会感到惊讶。
罗迪

17
我同意,只是我将其表述为“什么编译器会引起问题,以及它们使用的语言领域是什么” ;-P
Steve Jessop,

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.