68

C++ 保证编译单元(.cpp 文件)中的变量按声明顺序进行初始化。对于编译单元的数量,此规则分别适用于每个编译单元(我的意思是类之外的静态变量)。

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

我在哪里可以看到关于 gcc 和 MSVC 的这个顺序的一些解释(我知道依赖它是一个非常糟糕的主意 - 这只是为了了解我们在迁移到新的 GCC 主要和不同操作系统时可能遇到的遗留代码问题) ?

4

7 回答 7

76

正如您所说,该顺序在不同的编译单元中是未定义的。

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

这是因为这不是在语言级别而是在链接器级别解决的。所以你真的需要查看链接器文档。尽管我真的怀疑这会以任何有用的方式提供帮助。

对于 gcc:查看ld

我发现即使更改链接的对象文件的顺序也可以更改初始化顺序。因此,您需要担心的不仅仅是链接器,还有构建系统如何调用链接器。甚至试图解决问题实际上也是徒劳的。

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

有一些技术可以解决这个问题。

  • 延迟初始化。
  • 施瓦茨计数器
  • 将所有复杂的全局变量放在同一个编译单元中。

  • 注 1:全局变量:
    用于泛指可能在 之前初始化的静态存储持续时间变量main()
  • 注意 2:可能
    在一般情况下,我们希望静态存储持续时间变量在 main 之前初始化,但编译器在某些情况下允许延迟初始化(规则很复杂,详见标准)。
于 2008-10-17T07:23:14.840 回答
20

我希望模块之间的构造函数顺序主要取决于您将对象传递给链接器的顺序。

但是,GCC 确实允许您使用init_priority显式指定全局 ctor的顺序:

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”。

于 2008-10-17T07:30:16.870 回答
17

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

这也有例外,我并不声称所有这些,但以下是我自己遇到的:

1) 4.7 之前的 GCC 版本实际上以链接行的相反顺序初始化。GCC 中的这张票是更改发生的时间,它破坏了许多依赖于初始化顺序的程序(包括我的!)。

2) 在 GCC 和 Clang 中,使用构造函数优先级可以改变初始化顺序。请注意,这只适用于声明为“构造函数”的函数(即它们应该像全局对象构造函数一样运行)。我尝试过使用这样的优先级,发现即使在构造函数上具有最高优先级,所有没有优先级的构造函数(例如普通全局对象、没有优先级的构造函数)都会首先被初始化。换句话说,优先级只是相对于其他具有优先级的功能而言,但真正的一等公民是那些没有优先级的。更糟糕的是,由于上述第 (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() 以生成静态库,并将生成的静态库添加为 target_link_libraries( ) 调用您的应用程序/DLL。请注意,在这种情况下,您的全局对象可能会被优化,您可以使用--whole-archive标志强制链接器不要删除该特定小型存档文件的未使用符号。

结束提示

要绝对知道链接的应用程序/共享库的初始化顺序,请将 --print-map 传递给 ld 链接器和 grep 用于 .init_array (或在 4.7 之前的 GCC 中, grep 用于 .ctors)。每个全局构造函数都将按照初始化的顺序打印,并记住在 4.7 之前的 GCC 中该顺序是相反的(参见上面的第 (1) 点)。

写这个答案的动机是我需要知道这些信息,别无选择,只能依赖初始化顺序,并且在其他 SO 帖子和互联网论坛中只发现了这些信息的一小部分。其中大部分是通过大量实验学到的,我希望这可以节省一些人这样做的时间!

于 2016-09-12T00:53:36.380 回答
4

http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12 - 这个链接移动。这个更稳定,但必须四处寻找它。

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

于 2008-10-17T07:41:03.323 回答
2

一个健壮的解决方案是使用返回对静态变量的引用的 getter 函数。下面显示了一个简单的示例,它是我们SDG 控制器中间件中的一个复杂变体。

// 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>可以保证在插入任何值之前对其进行初始化。

于 2020-07-14T13:28:59.703 回答
1

除了来自 C 背景的 Martin 的评论,我一直认为静态变量是程序可执行文件的一部分,在数据段中合并和分配空间。因此,静态变量可以被认为是在程序加载时初始化,在任何代码执行之前。可以通过查看链接器输出的映射文件的数据段来确定发生这种情况的确切顺序,但对于大多数意图和目的而言,初始化是同时进行的。

编辑:根据静态对象的构造顺序可能是不可移植的,应该避免。

于 2008-10-17T07:38:10.193 回答
0

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

于 2015-04-25T09:54:38.260 回答