静态变量初始化顺序

71

C++保证编译单元(.cpp文件)中的变量按声明顺序初始化。对于多个编译单元,此规则适用于每个单独的编译单元(我指类外的静态变量)。

但是,跨不同编译单元的变量初始化顺序是未定义的。

在哪里可以看到有关gcc和MSVC的初始化顺序解释(我知道依赖它是一个非常糟糕的想法 - 这只是为了理解当将遗留代码移动到新的GCC主要版本和不同的操作系统时可能遇到的问题)?

7个回答

79

正如你所说,订单在不同的编译单元之间是未定义的。

在同一编译单元中,顺序是明确定义的:与定义的顺序相同。

这是因为这不是在语言级别上解决的,而是在链接器级别上解决的。因此,您确实需要查看链接器文档。但我真的怀疑这样做会有任何有用的帮助。

对于gcc:请查看ld

我发现即使更改链接的对象文件的顺序也会更改初始化顺序。因此,您不仅需要担心链接器,还需要担心构建系统如何调用链接器。即使尝试解决问题也几乎是不可能的。

这通常只在初始化其自身初始化期间引用彼此的全局变量时才会出现问题(因此仅影响具有构造函数的对象)。

有技术可以绕过该问题。

  • 延迟初始化。
  • Schwarz Counter
  • 将所有复杂的全局变量放在同一个编译单元中。

  • 注1:全局变量:
    宽泛地用于指潜在在main()之前初始化的静态存储期变量。
  • 注2:可能
    在一般情况下,我们希望在main之前初始化静态存储期变量,但编译器允许在某些情况下延迟初始化(规则很复杂,请参阅标准以获取详细信息)。

7
我个人的偏好是将所有全局变量放在同一个编译单元中... :-) - paercebal
13
最好完全不需要使用全局变量。 - Martin York
4
你们两个都是对的,但不幸的是几代程序员并不知道这一点,他们编写了许多库和第三方代码,而我们现在需要使用它们... - Dmitry Khalatov
4
@AdN:是的,静态初始化和动态初始化之间有区别。静态初始化发生在动态初始化之前(因为它是在编译时完成并嵌入到底层汇编段(BSS块等)中)。当人们谈论初始化顺序时,我们只涉及动态部分(必须在运行时执行代码才能初始化的部分(C++11constexpt基本上是另一个编译器时间常量))。这不应该导致关于此事的任何争论发生变化或导致问题出现。你可以提出一个问题,以便我们可以更详细地探讨。 - Martin York
1
@MartinYork 这个问题不是关于全局变量,而是关于静态变量,它们同样可以在命名空间或类范围内。 - j b
显示剩余4条评论

21

我认为模块之间的构造函数顺序主要取决于将对象按照什么顺序传递给链接器。

然而,GCC允许您使用 init_priority 来显式指定全局构造函数的顺序:

class Thingy
{
public:
    Thingy(char*p) {printf(p);}
};

Thingy a("A");
Thingy b("B");
Thingy c("C");

输出 'ABC' ,正如你所期望的那样,但是

Thingy a __attribute__((init_priority(300))) ("A");
Thingy b __attribute__((init_priority(200))) ("B");
Thingy c __attribute__((init_priority(400))) ("C");

输出'BAC'。


2
一个新的明确指定链接,目前可以使用。 - Doncho Gunchev
我使用了这个属性,但是gcc给出了警告:"警告:所请求的初始优先级为内部使用保留"。虽然是警告,但我仍然被允许这样做。是否还有其他设置初始化优先级的方法? - Andrew Falanga
@Andrew - 你不应该使用 initfiniinit_priority。相反,使用 constructor 属性。还有一个 destructor 属性。你可能需要在其他编译器和平台上使用 initfini,但对于 GCC,你不需要使用它。另请参阅 GCC 邮件列表上的 *"Clarification of attribute init_priority"*。 - jww
1
@jww constructordestructor并不相关:它们存在是为了在main()之前和之后强制调用某些函数。它们与特定对象的初始化顺序无关,对于这一点,init_priority是恰当的工具。安德鲁看到的警告可能是因为他使用了一个不符合init_priority文档规定的“101到65535之间(包括边界)”的值;因此可以得出结论,超出该范围的值是被保留的,正如警告所清楚地表明的那样。我认为这两件事与问题无关。 - underscore_d
我也不认为依赖于非标准的、编译器特定的属性是一个好的实践。但是,知道这些选项还是很不错的。另一个想法是,改变传递给链接器的翻译单元的顺序将改变它们之间动态初始化的顺序,这似乎是一种没有根据的导致代码极其脆弱的方法。 - underscore_d

20

既然您已经知道除非绝对必要,否则不应依赖此信息,那么它来了。 我在各种工具链(MSVC、gcc / ld、clang / llvm等)中的一般观察是,将目标文件传递给链接器的顺序就是它们初始化的顺序。

虽然有例外,但我不宣称全部知道,但这里是我自己遇到的例子:

1)GCC 4.7之前的版本实际上以与链接行相反的顺序进行初始化。 GCC中的此票 就是变化发生的地方,并且它破坏了很多依赖于初始化顺序的程序(包括我的!)。

2)在GCC和Clang中,使用构造函数优先级可以更改初始化顺序。请注意,这仅适用于声明为“构造函数”的函数(即它们应该像全局对象构造函数一样运行)。我尝试过像这样使用优先级,并发现即使在构造函数的最高优先级下,所有没有优先级的构造函数(例如普通全局对象、没有优先级的构造函数)也将首先初始化。换句话说,优先级只是相对于具有优先级的其他函数,但真正的一等公民是没有优先级的那些。更糟糕的是,由于上述第1点,在GCC 4.7之前实际上相反。

3) 在Windows上,存在一个非常简洁实用的共享库(DLL)入口函数叫做DllMain()。如果定义了这个函数,它将在全局数据初始化完成之后直接运行,参数"fdwReason"等于DLL_PROCESS_ATTACH,而在消费应用程序有机会调用DLL上的任何函数之前运行。这在某些情况下非常有用,在GCC或Clang中使用C或C++时绝对没有类似的行为。最接近的方法是使用优先级构造函数(参见上面的第2点),但这绝对不是相同的东西,并且对于许多DllMain()适用的用例无法工作。

4) 如果你正在使用CMake生成你的构建系统(我经常使用),我发现输入源文件的顺序将是它们产生的目标对象文件在链接器中的顺序。然而,通常情况下,您的应用程序/DLL还会链接其他库,在这种情况下,那些库将被放置于链接线路的源文件之后。如果你想让其中一个全局对象成为第一个进行初始化的对象,那么你很幸运,只需将包含该对象的源文件放在源文件列表的第一个位置即可。但如果你想让其中一个全局对象成为最后一个进行初始化的对象(这实际上可以复制DllMain()的行为!),那么你可以调用add_library()函数将该源文件生成一个静态库,并将生成的静态库作为应用程序/DLL的target_link_libraries()调用中的最后一个链接依赖项添加。请注意,此情况下全局对象可能会被优化掉,你可以使用--whole-archive标志来强制链接器不要删除该特定小型存档文件中未使用的符号。

结束语

要完全了解链接应用程序/共享库的初始化顺序,请将--print-map传递给ld链接器,并通过grep查找.init_array(或在GCC 4.7之前,查找.ctors)。每个全局构造函数将按其初始化顺序打印出来,并且请记住,在GCC 4.7之前,顺序是相反的(参见上面的第(1)点)。

编写此答案的动机是我需要了解此信息,别无选择,只能依靠初始化顺序,并发现其他SO帖子和互联网论坛中只有零星的信息。其中大部分是通过大量实验学习的,我希望这可以为某些人节省时间!


换句话说,优先级只是相对于具有优先级的其他函数,真正的一等公民是那些没有优先级的函数。哇,这是一个可怕的设计选择。 - Joseph Garvin
我其实不确定它是否仍然有效,我有一些GCC9代码,似乎通过在一个变量上设置init_priority(101)来修复了问题,但根据这个说法,这应该没有任何影响。 - Joseph Garvin
@JosephGarvin 可能是这样,但文档仍然存在歧义:https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Attributes.html#C_002b_002b-Attributes,并且这个关于歧义的开放问题尚未解决或争议:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65115我已经有一段时间没有测试过这个功能了,但我很想看到行为改变的证据以及它出现的版本,以便我可以更新这个答案来反映它。 - Nicholas Smith

4

这个链接与其他一些链接相比较可能稍有变动。但是这个链接相对更加稳定,不过您需要搜寻一下。

编辑:osgx提供了一个更好的链接


网档案中有副本:http://web.archive.org/web/20080512011623/http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12;Marshall Cline的C++ FAQ Lite中的“[10.12]什么是“静态初始化顺序混乱”?”部分。类似的部分在https://isocpp.org/wiki/faq/ctors中也有。 - osgx

2

一个健壮的解决方案是使用一个getter函数,该函数返回一个静态变量的引用。下面是一个简单的示例,在我们的SDG Controller中间件中有一个复杂的变体。

// Foo.h
class Foo {
 public:
  Foo() {}

  static bool insertIntoBar(int number);

 private:
  static std::vector<int>& getBar();
};

// Foo.cpp
std::vector<int>& Foo::getBar() {
  static std::vector<int> bar;
  return bar;
}

bool Foo::insertIntoBar(int number) {
  getBar().push_back(number);
  return true;
}

// A.h
class A {
 public:
  A() {}

 private:
  static bool a1;
};

// A.cpp
bool A::a1 = Foo::insertIntoBar(22);

初始化过程从唯一的静态成员变量bool A::a1开始。然后调用Foo::insertIntoBar(22),这将调用Foo::getBar(),在返回初始化对象的引用之前,静态std::vector<int>变量的初始化将会发生。
如果static std::vector<int> bar直接作为Foo类的成员变量放置,取决于源文件的命名顺序,存在barinsertIntoBar()被调用后初始化从而导致程序崩溃的可能性。
如果多个静态成员变量在初始化期间调用insertIntoBar(),则它们的顺序不依赖于源文件的名称,即随机的,但是std::vector<int>将保证在任何值被插入之前初始化。

这是“单例”函数模型,对于创业公司来说是一个很好的解决方案,只有很少的运行时开销。需要注意的问题是单例的关闭:如果一个单例对象在其运行时方法(在自身构造之后)创建另一个子单例,那么子单例将在父单例之前被销毁。如果父析构函数调用子函数,则会因为子对象已经被销毁而失败! - Gem Taylor
完全同意。这主要针对资源有限的嵌入式设备,以及在断电或reset()调用时关机。 - Moritz

1
除了Martin的评论之外,作为一个来自C语言背景的人,我总是认为静态变量是程序可执行文件的一部分,在数据段中分配和分配空间。因此,可以将静态变量视为在程序加载时初始化,而不是在执行任何代码之前。可以通过查看链接器输出的映射文件的数据段来确定这发生的确切顺序,但对于大多数意图而言,初始化是同时进行的。
编辑:根据静态对象的构建顺序可能是不可移植的,应该避免使用。

1
当你有C++类的构造函数具有副作用(比如,它们相互引用)时,就会出现问题。 - Mike F
个人而言,我尽可能避免这种情况发生,因为我的经验(或许是缺乏知识)并不好。通常,我要么将大部分构造移至在启动时调用的 Init 函数中,要么将其从静态变量更改为在启动时初始化的全局指针。 - SmacL
1
@smacl:当然,但是你必须处理和Finalize函数以释放数据,并处理有时候Init和Finalize都会被多次调用的情况,以及有时候并发调用的情况。在这里,RAII习惯用法结合DLL中全局变量的自动初始化是非常受欢迎的。 - paercebal
1
常量和动态初始化静态对象有单独的段。前者在后者之前初始化,并且可以被编译成可执行文件。而后者则不能,并且只有在相同的翻译单元中才能依赖它们的初始化顺序(即定义顺序)。如果它们跨越不同的翻译单元,则依赖它们的顺序不是“可能有问题”,而是绝对有问题的。 :P - underscore_d

0
如果你真的想知道最终的顺序,我建议你创建一个类,其构造函数记录当前时间戳,并在每个cpp文件中创建几个静态实例,这样你就可以知道初始化的最终顺序。确保在构造函数中放置一些耗时操作,以便不会为每个文件获取相同的时间戳。

这样做不会教授任何有用的东西,因为顺序在形式上是未定义的,所以学习它如何在某一天的一个链接器上偶然排序 - 以依赖于结果的非知识为目标 - 这是一个导致代码脆弱并在下一个环节崩溃的方法。我想对于那些闲暇地研究特定链接器工作方式的人可能会有趣,但我们中有多少人这样做呢? - underscore_d
此外,随着动态重链接器的发明(很久以前),实际上无法保证静态构建顺序在每次运行时都相同。 - Gem Taylor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接