静态变量初始化顺序
Posted
技术标签:
【中文标题】静态变量初始化顺序【英文标题】:Static variables initialisation order 【发布时间】:2010-09-17 16:41:57 【问题描述】:C++ 保证编译单元(.cpp 文件)中的变量按声明顺序进行初始化。对于编译单元的数量,此规则适用于每个单独的(我的意思是类之外的静态变量)。
但是,变量的初始化顺序在不同的编译单元之间是未定义的。
我在哪里可以看到关于 gcc 和 MSVC 的这个顺序的一些解释(我知道依赖它是一个非常糟糕的主意 - 这只是为了了解我们在迁移到新的 GCC 专业时遗留代码可能遇到的问题和不同的操作系统)?
【问题讨论】:
【参考方案1】:正如你所说,不同编译单元之间的顺序是不确定的。
在同一个编译单元内,顺序是很好定义的:与定义相同的顺序。
这是因为这不是在语言级别而是在链接器级别解决的。所以你真的需要查看链接器文档。虽然我真的怀疑这是否会有所帮助。
对于 gcc:查看 ld
我发现即使更改链接的对象文件的顺序也可以更改初始化顺序。因此,您需要担心的不仅仅是链接器,还有构建系统如何调用链接器。甚至尝试解决问题实际上是行不通的。
这通常仅在初始化在其自身初始化期间相互引用的全局变量时才会出现问题(因此仅影响具有构造函数的对象)。
有一些技巧可以解决这个问题。
延迟初始化。 Schwarz Counter 将所有复杂的全局变量放在同一个编译单元中。注 1:全局变量: 泛指在
main()
之前可能初始化的静态存储持续时间变量。
注 2:可能
在一般情况下,我们希望静态存储持续时间变量在 main 之前初始化,但在某些情况下允许编译器延迟初始化(规则很复杂,详见标准)。
【讨论】:
我自己的偏好适用于同一编译单元中的所有全局变量... :-) 最好根本不需要全局变量。 你们俩都对,但不幸的是,这对于编写大量库和我们必须使用的第 3 方代码的几代程序员来说是未知的...... @AdN:是的,静态初始化和动态初始化是有区别的。静态发生在动态之前(因为它是在编译时完成并烘焙到底层程序集段(BSS 块等)中)。当人们谈论初始化顺序时,我们仅指的是动态部分(必须在运行时执行代码才能初始化的部分(C++11constexpt
基本上是另一个编译器时间常数))。这不应该导致任何关于此的争论改变或导致某些东西咬你。您能否提出一个问题,以便我们更详细地探讨。
@jb 更新为文本中的注释。【参考方案2】:
我希望模块之间的构造函数顺序主要取决于您将对象传递给链接器的顺序。
但是,GCC 确实允许您 use init_priority
to explicitly specify the ordering 为全球 ctors:
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”。
【讨论】:
一个新的explicitly specify 链接现在可以使用。 我使用了这个属性,但 gcc 给出了“警告:请求的 init_priority 保留供内部使用”。作为一个警告,我仍然被允许这样做。还有其他设置初始化优先级的方法吗? @Andrew - 你应该不使用init
、fini
或init_priority
。相反,请使用constructor
attribute。还有一个destructor
属性。您可能需要在其他编译器和平台上使用 init
和 fini
,但对于 GCC,您不会使用它。另请参阅 GCC 邮件列表上的 "Clarification of attribute init_priority"。
@jww constructor
和 destructor
不相关:它们的存在是为了分别强制在 main()
之前和之后调用某些函数。它们与特定对象的初始化顺序无关,init_priority
恰恰是正确的工具。 Andrew 看到的警告可能是因为他使用的值不是“在 101 和 65535 之间有界”,如init_priority
的文档中规定的那样;因此,正如警告明确指出的那样,人们可能会得出结论,超出该范围的值是保留的。我认为这两件事都与这个问题无关。
...我也不认为像这样依赖非标准的、特定于编译器的属性是一种好习惯。但是,嘿,很高兴了解这些选项。另一个想法,即更改传递给链接器的翻译单元的顺序将改变它们之间的动态初始化顺序,这似乎是一个非常脆弱的代码的没有根据的秘诀。【参考方案3】:
既然您已经知道除非绝对必要,否则您不应依赖此信息,所以它来了。我对各种工具链(MSVC、gcc/ld、clang/llvm 等)的一般观察是,将目标文件传递给链接器的顺序就是初始化它们的顺序。
这也有例外,我并不声称所有这些,但以下是我自己遇到的:
1) 4.7 之前的 GCC 版本实际上以链接行的相反顺序初始化。 This ticket in GCC 是发生变化的时候,它破坏了很多依赖于初始化顺序的程序(包括我的!)。
2) 在 GCC 和 Clang 中,constructor function priority 的使用可以改变初始化顺序。请注意,这仅适用于声明为“构造函数”的函数(即它们应该像全局对象构造函数一样运行)。我尝试过使用这样的优先级,发现即使在构造函数上具有最高优先级,所有没有优先级的构造函数(例如普通全局对象、没有优先级的构造函数)都会被初始化first。换句话说,优先级只是相对于其他有优先级的功能而言,但真正的一等公民是那些没有优先级的。更糟糕的是,由于上述第 (1) 点,此规则在 4.7 之前的 GCC 中实际上是相反的。
3) 在 Windows 上,有一个非常简洁和有用的共享库 (DLL) 入口点函数,称为 DllMain(),如果定义了该函数,它将在所有全局数据都已完成后直接使用等于 DLL_PROCESS_ATTACH 的参数“fdwReason”运行已初始化并且在消费应用程序有机会调用 DLL 上的任何函数。这在某些情况下非常有用,并且在使用 GCC 或使用 C 或 C++ 的 Clang 的其他平台上绝对没有类似的行为。您会发现最接近的是创建一个具有优先级的构造函数(参见上面的第 (2) 点),这绝对不是一回事,并且不适用于 DllMain() 工作的许多用例。
4) 如果您使用 CMake 来生成构建系统,我经常这样做,我发现输入源文件的顺序将是它们提供给链接器的结果目标文件的顺序。但是,通常您的应用程序/DLL 也会链接到其他库,在这种情况下,这些库将位于您输入源文件之后的链接行。如果您希望您的全局对象之一是要初始化的第一个,那么您很幸运,您可以将包含该对象的源文件放在源列表中的第一个文件。但是,如果您希望将一个最后一个初始化(这可以有效地复制 DllMain() 行为!),那么您可以使用该源文件调用 add_library()生成一个静态库,并将生成的静态库添加为应用程序/DLL 的 target_link_libraries() 调用中的最后一个链接依赖项。请注意,在这种情况下您的全局对象可能会被优化,您可以使用 --whole-archive 标志强制链接器不要删除该特定小型存档文件的未使用符号。
结束提示
要绝对了解链接的应用程序/共享库的初始化顺序,请将 --print-map 传递给 ld 链接器,并将 grep 传递给 .init_array(或在 4.7 之前的 GCC 中,将 grep 传递给 .ctors)。每个全局构造函数都将按照初始化的顺序打印,记住在 4.7 之前的 GCC 中顺序是相反的(参见上面的第 (1) 点)。
写这个答案的动机是我需要知道这些信息,别无选择,只能依赖初始化顺序,并且在其他 SO 帖子和互联网论坛中只发现了这些信息的一小部分。其中大部分是通过大量实验学到的,我希望这可以节省一些人这样做的时间!
【讨论】:
【参考方案4】:http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12 - 此链接移动。这个one 更稳定,但你必须四处寻找。
编辑:osgx 提供了更好的link。
【讨论】:
网络存档中有副本:http://web.archive.org/web/20080512011623/http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12; “[10.12] 什么是“静态初始化命令惨败”? Marshall Cline 的 C++ FAQ Lite 部分。类似的部分在isocpp.org/wiki/faq/ctors【参考方案5】:一个可靠的解决方案是使用一个 getter 函数来返回对静态变量的引用。一个简单的例子如下所示,我们SDG Controller middleware中的一个复杂变体。
// 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 class
的成员变量放置,则根据源文件的命名顺序,bar
可能会在调用insertIntoBar()
之后被初始化,从而使程序崩溃。
如果多个静态成员变量在初始化期间调用insertIntoBar()
,则顺序将不依赖于源文件的名称,即随机,但std::vector<int>
将保证在任何值被初始化之前被初始化插入其中。
【讨论】:
这是“singleton”函数模型,它是一个很好的启动修复,只有一点运行时开销。单例需要注意的问题是关闭:如果一个单例对象在其运行时方法中创建另一个子单例(在其自己的构造之后),则该子单例将在父单例之前被销毁。如果父析构函数调用子析构函数,它将会失败,因为子析构已经被破坏了! 完全同意。这主要针对资源有限的嵌入式设备以及关闭电源丢失或 reset() 调用的情况【参考方案6】:除了 Martin 的 cmets,来自 C 背景,我一直认为静态变量是程序可执行文件的一部分,在数据段中合并和分配空间。因此,静态变量可以被认为是在程序加载时被初始化,在任何代码被执行之前。发生这种情况的确切顺序可以通过查看链接器输出的映射文件的数据段来确定,但对于大多数意图和目的而言,初始化是同时进行的。
编辑:根据静态对象的构造顺序可能是不可移植的,应该避免。
【讨论】:
当你的 C++ 类的构造函数有副作用(比如,它们相互引用)时,就会出现问题。 就我个人而言,我尽量避免这种情况,因为我对此的经验(可能缺乏知识)并不好。通常我要么将大部分构造移动到一个在启动时调用的 Init 函数,要么从静态更改为在启动时在堆上初始化的全局指针。 @smacl :当然,但是你必须处理和 Finalize 函数来释放数据,并处理有时 Init 和 Finalize 被多次调用的事实,有时,同时调用。 RAII 习语在这里结合了 DLL 中全局变量的自动初始化,非常受欢迎 对于常量和动态初始化的静态对象有不同的段。前者在后者之前被初始化并且可以被烘焙到可执行文件中。后者不能,只有当它们在同一个翻译单元中时,才能依赖于它们的初始化顺序(==定义的顺序)。如果它们跨越不同的 TU,那么依赖它们的顺序并不是“可能”是不好的做法:这绝对是不好的。 :P【参考方案7】:如果您真的想知道最终顺序,我建议您创建一个类,其构造函数记录当前时间戳,并在每个 cpp 文件中创建该类的多个静态实例,以便您知道初始化的最终顺序.确保在构造函数中放置一些耗时的操作,这样您就不会为每个文件获得相同的时间戳。
【讨论】:
这样做不会教任何有用的东西,因为订单在形式上是未定义的,因此学习它如何在某一天偶然在一个链接器上订购 - 目的是依赖于由此产生的非知识 - 是下一个崩溃的脆弱代码的秘诀。我想对于那些无所事事地研究给定链接器如何做事的人来说可能会很有趣,但我们当中有多少人这样做呢? 另外,随着动态重新链接器的发明(很久以前),实际上并不能很好地保证运行之间的静态构造顺序相同。以上是关于静态变量初始化顺序的主要内容,如果未能解决你的问题,请参考以下文章