这就是标准所说的:
具有静态存储持续时间的变量在程序启动时被初始化。变量与
线程存储持续时间被初始化为线程执行的结果。在这些阶段的每一个中
初始化,初始化发生如下。
[...] 如果具有静态或线程存储持续时间的变量或临时对象由实体的常量初始化程序初始化,则执行常量初始化。如果不执行常量初始化,则具有静态存储持续时间 (6.7.1) 或线程存储持续时间 (6.7.2) 的变量被零初始化 (11.6)。零初始化和常量初始化合称为静态初始化;所有其他初始化都是动态初始化。所有静态初始化都强烈发生在(4.7.1)任何动态初始化之前。 [ 注意: 非局部变量的动态初始化在 6.6.3 中描述;那个
局部静态变量在 9.7 中描述。 —尾注 ]
允许实现对具有静态或线程存储持续时间的变量进行初始化作为静态初始化,即使这种初始化不需要静态完成,前提是
- 动态版本的初始化不会改变任何其他静态对象的值或
初始化之前的线程存储持续时间,以及
- 如果所有不需要静态初始化的变量都被动态初始化,则静态版本的初始化在初始化变量中产生的值与动态初始化产生的值相同。
[ 注意: 因此,如果对象 obj1 的初始化引用了命名空间范围的对象 obj2,可能需要动态初始化并稍后在同一翻译单元中定义,它未指定所使用的obj2 的值是完全初始化的obj2 的值(因为obj2 是静态初始化的)还是只是零初始化的obj2 的值。例如,
inline double fd() { return 1.0; }
extern double d1;
double d2 = d1; // unspecified:
// may be statically initialized to 0.0 or
// dynamically initialized to 0.0 if d1 is
// dynamically initialized, or 1.0 otherwise
double d1 = fd(); // may be initialized statically or dynamically to 1.0
—尾注 ]
[...]
如果[某些条件] V 在单个翻译单元中定义在W 之前,则V 的[动态] 初始化在W 的初始化之前排序。
从概念上讲,静态初始化是在翻译时执行的:编译器发出一个符号,其值是已经初始化的值。在某些情况下,这将是 0;在某些情况下,这将是评估常量表达式初始化程序和/或为变量调用 constexpr 构造函数的结果。如果需要进行任何动态初始化——因为变量的实际初始化不满足常量初始化的条件——那么编译器会发出一段代码,按照定义顺序初始化该翻译单元中的变量。链接器获取所有这些执行动态初始化的代码,并以某种顺序(可能是交错的)组合它们。
没有无限递归,因为a的动态初始化并没有启动b的动态初始化;它只是使用 b 已有的任何值,或者因为 b 已经动态初始化,或者因为它仍然具有静态初始化的值。 反之亦然。如果b在a之前被动态初始化---并且你不能保证这一点,因为这两个变量是在不同的翻译单元中定义的---那么在b的动态初始化时,a值为 0,所以b 变为 1;那么当a被动态初始化时,它的值变成了2,所以你看到了2 1的结果。但是如果a在b之前动态初始化,你会看到1 2。
在只有一个翻译单元的情况下,b 的动态初始化必须在a 之前进行,因为单个翻译单元内的动态初始化按定义顺序(而不是声明)发生。这解释了您所看到的结果2 1。然而,2 1 的这个结果仍然不能保证,因为允许静态地进行动态初始化。编译器可以选择静态地给a 值2,因为如果它被动态初始化,它就会有这个值。如果编译器选择使a 的初始化完全静态,但没有为b 选择,那么b 的动态初始化将给它值3。
如果有两个不同的翻译单元呢?此处标准的措辞尚不清楚,但我的解释是允许将a 或b 中的一个或两个完全静态初始化为基于任何有效的动态初始化顺序可能具有的任何有效值!如果只有a 完全静态初始化,它可以静态初始化为1 或2,导致b 在动态初始化期间分别变为2 或3。同样,如果只有 b 完全静态初始化,它可以静态初始化为 1 或 2,导致 a 分别变为 2 或 3。所以:
- 对于第一个程序,可能的结果是
1 2、2 1、2 3 或3 2。
- 对于第二个程序,可能的结果是
2 1 和2 3。
我认为在实践中,将任一变量的值设为 3 的编译器会使一些用户非常生气,并且可能会停止这样做。尽管如此,理论上的可能性仍然存在。
避免不可预知的初始化顺序问题的一种方法是禁止非局部静态变量的非常量初始化器。在这种情况下,不可能发生动态初始化,因此所有非局部静态变量的初始化都以明确定义的顺序发生并产生明确定义的值,实际上很可能在编译时进行评估。