全局变量的循环依赖性与外部说明符



全局变量可以在不使用存储类说明符定义的情况下声明extern。所以我相信可以为全局变量引入循环依赖关系,就像使用前向声明使类/模块相互依赖一样。链接器如何处理变量定义之间的此类依赖关系?这种做法是否会产生未定义的行为?

//source2.cpp
extern int b;
int a = b + 1;
//source1.cpp
#include<iostream>
extern int a;
int b = a + 1;
int main() {
std::cout << a << " " << b <<std::endl;
}

甚至,

#include<iostream>
extern int a;
int b = a + 1;
int a = b + 1;
int main() {
std::cout << a << " " << b <<std::endl;
}

两者都打印出 2 1。 发生了什么事情?我想链接器将外部符号int a解析为值为 0。 但是,它是如何决定外部符号求解完成的,而不是永远停留在对变量定义的递归搜索中呢?

这是标准必须说的:

具有静态存储持续时间的变量作为程序启动的结果进行初始化。变量与线程存储持续时间根据线程执行进行初始化。在以下每个阶段中 启动,初始化发生如下。

[...]如果具有静态或线程存储持续时间的变量或临时对象由实体的常量初始值设定项初始化,则执行常量初始化。如果未执行常量初始化,则具有静态存储持续时间 (6.7.1) 或线程存储持续时间 (6.7.2) 的变量初始化为 0 (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

尾注]

[...]

如果在单个翻译单元中W之前定义了 [某些条件]V,则V的 [动态] 初始化在初始化之前W

进行排序。

从概念上讲,静态初始化是在转换时执行的:编译器发出一个符号,其值是已初始化的值。在某些情况下,这将为 0;在某些情况下,它将是计算常量表达式初始值设定项和/或为变量调用 constexpr 构造函数的结果。如果需要执行任何动态初始化---因为变量的实际初始化不满足常量初始化的条件---则编译器会发出一段代码,按定义顺序初始化该转换单元中的变量。链接器获取所有这些执行动态初始化的代码段,并按某种顺序(可能是交错的)将它们组合在一起。

没有无限递归,因为a的动态初始化不会启动b的动态初始化;它只是使用b已经拥有的任何值,要么是因为b已经动态初始化,要么是因为它仍然具有静态初始化的值。反之亦然。如果ba---并且由于两个变量在不同的翻译单元中定义,因此您无法保证这一点---那么在b的动态初始化时,a的值为0,因此b变为1;然后,当动态初始化a时,其值变为 2,因此您会看到结果2 1。但是如果ab之前动态初始化,你会看到1 2

在只有一个翻译单元的情况下,b的动态初始化必须在a之前进行,因为单个翻译单元中的动态初始化是按定义顺序(而不是声明)发生的。这解释了您所看到的结果2 1。但是,由于允许静态完成动态初始化的规定,仍然不能保证2 1的结果。编译器可以选择静态地为a提供值 2,因为这是动态初始化时的值。如果编译器选择使a的初始化完全静态,但没有选择b,那么b的动态初始化将给它值3。

两个不同翻译单元的情况如何?这里的标准措辞不清楚,但我的解释是,允许它完全静态地初始化一个或两个,ab到它基于任何有效的动态初始化顺序可以拥有的任何有效值!如果只有a完全静态初始化,则可以静态初始化为 1 或 2,导致b在动态初始化期间分别变为 2 或 3。同样,如果只有b完全静态初始化,则可以静态初始化为 1 或 2,导致a分别变为 2 或 3。所以:

  • 对于第一个程序,可能的结果是1 22 12 33 2
  • 对于第二个程序,可能的结果是2 12 3.

我认为在实践中,给任何一个变量值为 3 的编译器会让一些用户非常生气,并且可能会停止这样做。尽管如此,理论上的可能性仍然存在。

避免不可预测初始化顺序问题的一种方法是禁止非局部静态变量的非常量初始值设定项。在这种情况下,不可能发生动态初始化,因此非局部静态变量的所有初始化都按明确定义的顺序进行,并产生明确定义的值,实际上很可能在编译时进行评估。

我认为您正在将实际上多个步骤描绘成一个步骤。让我们来看看会发生什么,从编译开始。我将重点介绍b的定义;a的处理方式类似。

编译
松散地说,当编译器看到"int b = a + 1;"时,它会做两件事。首先,它留出足够的内存来存储int。此内存位置被注释为"链接器注意:这是名为"b"的内存位置。其次,编译器生成类似于以下内容的注释指令,这些指令将在初始化全局变量时执行。
1) 读取存储在<<strong>链接器注意:在此处插入a的地址>中的值。
2) 添加1
3)将结果写入b

链接链接
器看到编译器生成的两个批注。从第一个开始,它能够计算b的地址,该地址被添加到链接器内部解析的符号名称列表中。完成此列表(跨所有翻译单元)后,链接器通过将a的地址放在请求的位置来处理第二个批注。查找此地址不需要超过链接器列表的标准二分搜索。(不保证递归。

执行
当程序运行时,它遵循编译器生成的指令,并由链接器修改。为所有全局变量和静态变量留出第一个内存。然后初始化该内存。当需要初始化b时,计算机将读取a位置中的任何值,添加1,并将结果写入b的位置。a是否已初始化不一定确定。(另见静态订单惨败。

最新更新