我在互联网上找到的所有关于静态初始化顺序惨败的内容都是关于 C++ 的,但是如果我初始化某种类型 Foo 的全局变量(例如
struct Foo {
int flag;
pthread_key_t key;
void *ptrs[10];
};
我无法初始化类型变量,struct Foo
如static struct Foo x = { 0 };
?如果我想因为 SIOF 获得正确的代码?
我在互联网上找到的所有关于静态初始化顺序惨败的内容都是关于 C++ 的,但是如果我初始化某种类型 Foo 的全局变量(例如
struct Foo {
int flag;
pthread_key_t key;
void *ptrs[10];
};
我无法初始化类型变量,struct Foo
如static struct Foo x = { 0 };
?如果我想因为 SIOF 获得正确的代码?
C++ 中初始化的问题是可执行代码可以在函数之前运行
main
,因此并不总是清楚此类代码的运行顺序。由于静态对象的构造函数,这在 C++ 中是必要的。另一方面,C 不允许代码在函数之外运行。静态对象的初始值设定项必须是可以在编译时计算的常量表达式。
这意味着像这样的初始化器
static struct Foo x = { 0 };
在 C 中是完全可以使用的。对于具有静态(和线程)存储持续时间的对象,C 仅声明它们在调用 main() 之前的某个时刻进行初始化。C 只允许将它们初始化为常量表达式。而在 C++ 中,对象可以具有构造函数,并且可以初始化为函数的结果。
如果我们查看在调用 main() 之前运行的“C 运行时”(CRT) 代码的“底层”,就变量而言,它只会初始化
.data
和.bss
。从那里就可以开始了。等效的 C++ 运行时并不是那么简单,因为它还启动构造函数调用等。由于 C++ 标准和程序员都没有指定特定的顺序,CRT 只会以某种主观的出现顺序来调用它们。如果此时对象之间存在初始化顺序依赖性,那么一切都会很快崩溃。C++ 还通过将静态初始化定义为适合两个子类别的所有内容来增加额外的复杂性:常量初始化和零初始化。然后将其他所有内容命名为动态初始化(不要与动态分配混淆)。动态初始化又带有出现顺序、排序等概念。
在您想要如何对结构进行零初始化的情况下,没有问题。
但是,在一般情况下,打开库(又称共享库)时,C 代码中可能会出现问题
.so
。这是因为共享库可能包含.init
加载库时运行的代码部分。因此,您必须想象两个共享库在其初始化例程中引用彼此的数据结构。
诚然,这超出了C语言的范围。但是,在处理共享库时,它与链接器的上下文相关。
C 没有静态初始化顺序的失败。在 C89 中,规则是:
因此,标量类型的静态变量只能使用单个常量表达式进行初始化。如果变量的类型是具有标量元素类型的数组,则每个初始值设定项都需要是常量表达式,依此类推。由于常量表达式既不会产生副作用,也不依赖于任何其他计算产生的副作用,因此更改常量表达式的计算顺序不会影响结果。此外,编译器可以简单地发出已经初始化的数据(即,在编译时评估这些常量表达式),因此当程序启动时,不需要进行静态初始化。
之前唯一可以计算的非常量表达式
main
是从 C 运行时调用的表达式。这就是为什么、、 和FILE
所指向的对象已经可供第一个语句使用,例如。标准 C 不允许用户注册自己的启动代码以在之前运行- 尽管 GCC 确实提供了一个名为(大概以 C++ 功能命名)的扩展,如果您愿意,您可以使用该扩展在 C 中重新创建静态初始化顺序失败。stdin
stdout
stderr
main
main
__constructor__
Stroustrup 在《C++ 的设计与演化》中写道,他的目标是让用户定义的类型在内置类型所在的任何地方都可用。这意味着 C++ 必须允许类类型的全局变量,这意味着它们的构造函数将在程序启动期间被调用。由于早期的 C++ 没有
constexpr
函数,因此此类构造函数调用永远不可能是常量表达式。于是,静态初始化顺序惨败就诞生了。在C++标准化过程中,执行静态初始化的顺序问题是一个有争议的话题。我想大多数人都会同意理想的情况是每个静态变量在使用之前都进行初始化。不幸的是,这需要当时不存在的链接器技术(并且可能仍然不存在?)。静态变量的初始化可能涉及函数调用,并且这些函数可能在另一个 TU 中定义,这意味着您需要执行整个程序分析才能成功地按依赖顺序对静态变量进行拓扑排序。值得注意的是,即使 C++ 可以这样设计,它仍然不能完全防止初始化顺序问题。想象一下,如果您有一些库,其中函数的前提条件
use
是该init
函数在过去的某个时刻被调用过。然后,如果您有一个静态变量,其初始化程序调用init
另一个其初始化程序调用use
,则存在编译器看不到的顺序依赖性。最终,有限的初始化顺序保证了我们在 C++98 中得到的结果是我们在这种情况下可以获得的最好结果。凭借无限的后见之明,也许有人会抗议说,如果没有函数,标准将是不完整的
constexpr
(并且应该要求静态变量仅具有常量初始化)。