【问题标题】:C++: When (and how) are C++ Global Static Constructors Called?C++:何时(以及如何)调用 C++ 全局静态构造函数?
【发布时间】:2010-11-19 06:22:53
【问题描述】:

我正在编写一些 C++ 代码,但我遇到了一个困扰我一段时间的问题...假设我在 Linux 主机上使用 GCC 编译 ELF 目标,全局静态在哪里构造函数和析构函数调用了吗?

我听说在 crtbegin.o 中有一个函数 _init,在 crtend.o 中有一个函数 _fini。这些是crt0.o调用的吗?或者动态链接器是否真的检测到它们在加载的二进制文件中的存在并调用它们?如果是这样,什么时候它真正调用它们?

我主要想知道,这样我就可以了解在我的代码在运行时加载、执行和卸载时幕后发生的事情。

提前致谢!

更新:我基本上是想弄清楚构造函数被调用的一般时间。我不想根据这些信息在我的代码中做出假设,这或多或少是为了更好地了解我的程序加载时在较低级别发生的事情。我知道这是特定于操作系统的,但我已尝试在此问题中缩小范围。

【问题讨论】:

    标签: c++ gcc static global constructor


    【解决方案1】:

    当谈到非本地静态对象时,没有太多保证。正如您已经知道的(这里也提到过),它不应该编写依赖于此的代码。静态初始化顺序惨败……

    静态对象经过两个阶段的初始化:静态初始化和动态初始化。前者首先发生并通过常量表达式执行零初始化或初始化。后者发生在所有静态初始化完成之后。例如,这是在调用构造函数时。

    一般来说,这个初始化发生在 main() 之前的某个时间。然而,与许多人的想法相反,C++ 标准并不能保证这一点。实际上可以保证的是,在使用与正在初始化的对象相同的翻译单元中定义的任何函数或对象之前完成初始化。请注意,这不是特定于操作系统的。这是 C++ 规则。这是标准的引述:

    对象的动态初始化(8.5、9.4、12.1、12.6.1)是否由实现定义 命名空间范围在 main 的第一条语句之前完成。如果初始化被推迟到某个时间点 在 main 的第一个语句之后的时间,它应该发生在第一次使用定义的任何函数或对象之前 在与要初始化的对象相同的翻译单元中

    【讨论】:

    • 这不是惨败。这只是您需要注意的事情。不要再这样引用 C++ Lite FAQ 了。
    • 您错误地引用了标准。在全局命名空间中,它们是在 main 之前构造的。在其他命名空间中,它们可能会被延迟初始化。
    • 我引用了标准,而不是 C++ FAQ。我只是使用了本次讨论中已经使用的“表达式”(恰好来自常见问题解答)。
    • 该标准确实提到了命名空间范围和全局范围。并且清楚地表明全局范围是全局命名空间范围的缩写。您关于 main 之前初始化的声明让我有点困惑。您能否通过指出标准为全局范围对象提供保证的位置向我澄清这一点?不是我怀疑你,只是我想知道它放在哪里以及如何放置。
    • 我没有标准(我今晚会检查确切的参考)。但是即使按照您的逻辑,全局命名空间中的所有静态非局部变量都将在 main() 之前构造。 main() 在全局命名空间中?您的代码片段保证命名空间中的所有非本地静态变量都是在调用该命名空间中的任何函数之前构建的。因此保证全局命名空间中的所有静态非局部变量都是在调用 main() 之前构造的!这也使它与具有相同行为的 C 向后兼容。
    【解决方案2】:

    这在很大程度上取决于编译器和运行时。对构建全局对象的时间做出任何假设都不是一个好主意。

    如果您有一个依赖于另一个已构建的静态对象的静态对象,这尤其是一个问题。

    这称为“static initialization order fiasco”。即使您的代码中并非如此,有关该主题的 C++Lite 常见问题解答文章也值得一读。

    【讨论】:

    • 我之前遇到过静态初始化问题——调试起来很糟糕!我已经更新了我的问题。
    • 只有 C++ Lite FAQ 称其为惨败。当您知道它存在时,这并不是什么大问题。 C++ Lite FAQ 充斥着“马歇尔”的个人观点,并非所有观点都是好的。
    【解决方案3】:

    这不是特定于操作系统的,而是特定于编译器的。

    你已经给出了答案,初始化在__init完成。

    对于第二部分,在 gcc 中,您可以通过附加到变量定义的__attribute__((init_priority(PRIORITY))) 来保证初始化顺序,其中PRIORITY 是一些相对值,首先初始化较小的数字。

    【讨论】:

      【解决方案4】:

      您拥有的受资助者:

      • 全局命名空间中的所有静态非本地对象都是在 main() 之前构造的
      • 另一个命名空间中的所有静态非本地对象都是在使用该命名空间中的任何函数/方法之前构造的(因此允许编译器潜在地延迟评估它们[但不要指望这种行为])。
      • 翻译单元中的所有静态非本地对象均按声明顺序构造。
      • 没有定义翻译单元之间的顺序。
      • 所有静态非本地对象都按创建的相反顺序销毁。 (这包括静态函数变量(在首次使用时延迟创建)。

      如果你有相互依赖的全局变量,你有两个选择:

      • 将它们放在同一个翻译单元中。
      • 将它们转换为在首次使用时检索和构造的静态函数变量。

      示例1:全局A的构造函数使用全局日志

      class AType
      {    AType()  { log.report("A Constructed");}};
      
      LogType    log;
      AType      A;
      
      // Or 
      Class AType() 
      {    AType()  { getLog().report("A Constructed");}};
      LogType& getLog()
      {
          static LogType  log;
          return log;
      }
      // Define A anywhere;
      

      示例全局 B 的析构函数使用全局日志

      在这里,您必须保证对象日志不会在对象 B 之前被销毁。这意味着日志必须在 B 之前完全构建(因为随后将应用相反的销毁顺序)。同样可以使用相同的技术。要么将它们放在同一个翻译单元中,要么使用函数来获取日志。

      class BType
      {    ~BType()  { log.report("B Destroyed");}};
      
      LogType    log;
      BType      B;   // B constructed after log (so B will be destroyed first)
      
      // Or 
      Class BType() 
      {    BType()    { getLog();}
           /*
            * If log is used in the destructor then it must not be destroyed before B
            * This means it must be constructed before B 
            * (reverse order destruction guarantees that it will then be destroyed after B)
            *
            * To achieve this just call the getLog() function in the constructor.
            * This means that 'log' will be fully constructed before this object.
            * This means it will be destroyed after and thus safe to use in the destructor.
            */
          ~BType()    { getLog().report("B Destroyed");}
      };
      LogType& getLog()
      {
          static LogType  log;
          return log;
      }
      // Define B anywhere;
      

      【讨论】:

      • 他专门询问 gcc。有一种方法可以保证翻译之间的初始化顺序,详见我的回答。
      • @drhirsch:我知道。但是,当语言支持如此简单的问题解决方案时,为什么还要使用编译器特定的 hack。
      • @Marting York:嗯,简单似乎相当主观;-) 对我来说 __attribute__((init_priority(PRIORITY))) 似乎更容易,而且我不认为这是一个丑陋的黑客。但这可能是个人口味。但不要忘记,一些 gcc 开发人员实际上编写了标准,所以有一点机会,其中一个扩展可能有一天会作为语言特性出现;-)
      • hack: => 依赖于硬件/操作系统/编译器特定功能的通用代码。
      • 命名空间与它无关:它是translation unit
      【解决方案5】:

      根据 C++ 标准,在使用其翻译单元的任何函数或对象之前调用它们。请注意,对于全局命名空间中的对象,这意味着它们在调用 main() 之前被初始化。 (请参阅 ltcmelo'sMartin's 的答案以获取详细信息和对此的讨论。)

      【讨论】:

      • 这在很多情况下都是正确的,但标准不能保证。在我的回答中检查标准中的引用。
      • @ltcmelo:谢谢,我不知道。我会相应地修改我的答案。
      • ltcmelo 错误地引用了标准。在全局命名空间中,它们是在 main() 之前构造的。在其他命名空间中,它们可能会被延迟初始化。
      • 我当然指的是你的旧答案,在你完全改变它之前(“你所知道的是它们在 main() 之前被调用”或类似的),你可能还记得。删除了我的评论,因为它不再适合了。
      • @drhirsch:我知道你指的是旧版本。我还是不明白。 :(
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-09
      • 1970-01-01
      • 2010-09-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多