我很好奇函数中静态变量的底层实现。
如果我声明一个基本类型的静态变量(char,int,double等),并给它一个初始值,我想编译器只是在程序的最开始设置该变量的值在调用main()
之前:
void SomeFunction();
int main(int argCount, char ** argList)
{
// at this point, the memory reserved for 'answer'
// already contains the value of 42
SomeFunction();
}
void SomeFunction()
{
static int answer = 42;
}
但是,如果静态变量是类的实例:
class MyClass
{
//...
};
void SomeFunction();
int main(int argCount, char ** argList)
{
SomeFunction();
}
void SomeFunction()
{
static MyClass myVar;
}
我知道在第一次调用该函数之前不会对其进行初始化。由于编译器无法知道第一次调用函数的时间,它是如何产生这种行为的?它本质上是在函数体中引入了一个if块吗?
static bool initialized = 0;
if (!initialized)
{
// construct myVar
initialized = 1;
}
答案 0 :(得分:12)
在我看到的编译器输出中,函数局部静态变量的初始化与您想象的完全相同。
请注意,通常这是不以线程安全的方式完成。因此,如果您具有可能从多个线程调用的静态本地函数,则应考虑到这一点。在调用任何其他函数之前,在主线程中调用一次函数通常可以解决问题。
我应该补充一点,如果本地静态的初始化是一个简单的常量,就像在你的例子中一样,编译器不需要经历这些旋转 - 它只能初始化图像中的变量或{{1像常规的静态初始化(因为你的程序无法区分)。但是如果用函数的返回值初始化它,那么编译器几乎必须测试一个标志,指示初始化是否已完成或等效。
答案 1 :(得分:11)
This question涵盖了类似的理由,但未提及线程安全性。对于它的价值,C ++ 0x将使函数静态初始化线程安全。
(参见C++0x FCD,6.7 / 4关于函数静态:“如果控件在初始化变量时同时进入声明,则并发执行应等待 完成初始化。“)
另一件未提及的事情是函数静态以其构造的相反顺序被破坏,因此编译器维护一个在关闭时调用的析构函数列表(这可能与atexit使用的列表相同或不同) )。
答案 2 :(得分:2)
你对所有事情都是对的,包括初始化标志作为常见的实现。这基本上是为什么静态本地的初始化不是线程安全的,以及为什么存在pthread_once。
一个小小的警告:编译器必须发出代码,这些代码“就像在第一次使用时构造静态局部变量一样”。由于整数初始化没有副作用(并且不调用用户代码),因此在初始化int时由编译器决定。用户代码不能“合法地”找出它的作用。
显然,您可以查看汇编代码,或激发未定义的行为并从实际发生的事情中扣除。但是,C ++标准并不认为这是一个有效的理由,声称这种行为不像“规范”所说的那样“好像”。
答案 3 :(得分:1)
我知道在第一次调用该函数之前不会对其进行初始化。由于编译器无法知道第一次调用函数的时间,它是如何产生这种行为的?它本质上是在函数体中引入了一个if块吗?
是的,这是正确的:而且,FWIW,它不一定是线程安全的(如果函数是由两个线程同时“第一次”调用的话)。
出于这个原因,您可能更喜欢在全局范围内定义变量(尽管可能在类或命名空间中,或者没有外部链接的静态)而不是在函数内部,因此它在程序启动之前初始化而没有任何运行时“如果”。
答案 4 :(得分:1)
另一个转折是在嵌入式代码中,其中run-before-main()代码(cinit / whatever)可以将预初始化的数据(静态和非静态)从const数据段复制到ram中,可能驻留在只读存储器。在代码可能无法从某种可以从中重新加载的后备存储(磁盘)运行的情况下,这很有用。同样,这不违反语言的要求,因为这是在main()之前完成的。
略微切线:虽然我没有看到它做得太多(在Emacs之外),但程序或编译器基本上可以在进程中运行代码并实例化/初始化对象,然后冻结并转储进程。 Emacs做了类似的事情来加载大量的elisp(即咀嚼它),然后将运行状态转储为工作可执行文件,以避免在每次调用时解析成本。