C ++保证编译单元(.cpp文件)中的变量按声明顺序初始化。对于编译单元的数量,此规则分别适用于每个(我的意思是类外的静态变量)。
但是,变量的初始化顺序在不同的编译单元中是未定义的。
我在哪里可以看到关于gcc和MSVC的这个订单的一些解释(我知道依赖于这是一个非常糟糕的想法 - 它只是为了理解我们在迁移到新的GCC主要和不同的OS)?
答案 0 :(得分:69)
正如您所说,订单在不同的编译单元中是未定义的。
在同一个编译单元中,订单定义明确:与定义相同的顺序。
这是因为这不是在语言级别解决,而是在链接器级别解决。所以你真的需要查看链接器文档。虽然我真的怀疑这会有所帮助。
对于gcc:查看 ld
我发现即使更改链接的对象文件的顺序也可以更改初始化顺序。因此,您需要担心的不仅仅是链接器,而是构建系统如何调用链接器。即使尝试解决问题实际上也是一个不起作用。
这通常只是初始化在自己初始化期间引用彼此的全局变量时的问题(因此只会影响具有构造函数的对象)。
main()
之前可能初始化的静态存储持续时间变量。答案 1 :(得分:17)
我希望模块之间的构造函数顺序主要取决于将对象传递给链接器的顺序。
然而,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'。
答案 2 :(得分:13)
既然你已经知道除非绝对必要,否则你不应该依赖这些信息。我对各种工具链(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(),如果已定义,将使用参数" fdwReason"在所有全局数据初始化之后直接等于DLL_PROCESS_ATTACH,并且之前消费应用程序有机会调用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帖子和互联网论坛中只发现了这些信息的稀疏位。其中大部分是通过大量实验学到的,我希望这可以节省一些人的时间!
答案 3 :(得分:4)
http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12 - 此链接四处移动。这个one更稳定,但你必须环顾四周。
编辑:osgx提供了更好的link。
答案 4 :(得分:1)
除了Martin的评论,来自C背景,我总是将静态变量视为程序可执行文件的一部分,在数据段中合并和分配空间。因此,在执行任何代码之前,可以将静态变量视为在程序加载时初始化。通过查看链接器输出的映射文件的数据段可以确定发生这种情况的确切顺序,但是对于大多数意图和目的,初始化是同时的。
编辑:根据静态物体的构造顺序,可能是不可携带的,应该避免使用。
答案 5 :(得分:0)
如果您真的想知道最终的顺序,我建议您创建一个类,其构造函数记录当前时间戳并在每个cpp文件中创建该类的几个静态实例,以便您可以知道最终的初始化顺序。确保在构造函数中放置一些耗时的操作,这样就不会为每个文件获取相同的时间戳。
答案 6 :(得分:0)
一个可靠的解决方案是使用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>
被初始化在将任何值插入之前。