【问题标题】:Cyclic dependency of global variables with extern specifier具有外部说明符的全局变量的循环依赖
【发布时间】:2019-09-07 01:36:23
【问题描述】:

可以使用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。 但它是如何决定外部符号求解完成的,而不是永远停留在递归搜索变量定义的过程中呢?

【问题讨论】:

标签: c++ dependencies extern


【解决方案1】:

这就是标准所说的:

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

[...] 如果具有静态或线程存储持续时间的变量或临时对象由实体的常量初始化程序初始化,则执行常量初始化。如果不执行常量初始化,则具有静态存储持续时间 (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 已经动态初始化,或者因为它仍然具有静态初始化的值。 反之亦然。如果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 的编译器会使一些用户非常生气,并且可能会停止这样做。尽管如此,理论上的可能性仍然存在。

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

【讨论】:

    【解决方案2】:

    我认为您将一个步骤想象为实际上是多个步骤。让我们看看会发生什么,从编译开始。我将重点介绍b的定义; a 的处理类似。

    编译
    粗略地说,当编译器看到“int b = a + 1;”时,它做了两件事。首先,它留出足够的内存来存储int。这个内存位置被注解为“链接器注意:这里的内存位置叫“b”。其次,编译器生成类似下面的注解指令,这些指令是在全局变量被调用时执行的
    1) 读取存储在链接器注意事项:在此处插入a的地址>。
    2) 添加1.
    3) 将结果写入b

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-07-20
      • 1970-01-01
      • 1970-01-01
      • 2016-05-14
      • 1970-01-01
      • 2011-12-20
      • 1970-01-01
      相关资源
      最近更新 更多