代码之家  ›  专栏  ›  技术社区  ›  Fire Lancer

C++控制全局对象的析构函数顺序

  •  4
  • Fire Lancer  · 技术社区  · 14 年前

    我有一个类(A),它在另一个类(B)的构造函数和析构函数中访问(通过静态方法间接地)静态变量(STL容器)。

    对象可以是全局的、全局常量、另一类的静态成员、存储在其他类中(它们本身可能具有全局或静态实例),或者基本上可以是C++对象的任何其他地方。

    如果一个对象是在B中的静态成员之前构造的,或者在B中的静态成员之后销毁的,那么它将在某个点上导致崩溃(通常是访问冲突)。

    是否有某种方法可以保证类A的所有实例(除了那些已经泄漏的实例,因为根据定义,“丢失”并且不会以任何方式被破坏)都是在B的静态变量之后构造并在其之前被破坏的?

    我已经看到了一些使一个特定变量在另一个变量之前/之后被构造/销毁的解决方案,但是不是给定类型的所有实例的一般情况,所以不确定如何处理这个问题。

    6 回复  |  直到 6 年前
        1
  •  8
  •   GManNickG    14 年前

    不,这就是 static-initialization fiasco . 未指定对象在进入main之前的构造顺序。唯一的保证就是它会发生。

    您可以做的是惰性初始化。这意味着您的对象在使用之前不会被初始化。例如:

    struct A { /* some data */ };
    struct B { B(void){ /* get A's data */ } };
    
    A& get_A(void)
    {
        static A instance;
        return instance;
    }
    
    B& get_B(void)
    {
        static B instance;
        return instance;
    }
    

    你用 get_A get_B 获取全局实例。部分在哪里 B 使用 A 应该使用 盖特加 以及你对 应该与 盖特B . 注意 盖特B 在您的情况下是可选的。

    第一次创建B时会发生什么?(全局或在函数中)构造函数将调用 盖特加 那是 在哪里? A 将被创建。这让您控制构建的顺序。

    注意,我想我把你的A和B颠倒了。

        2
  •  1
  •   Seva Alekseyev    14 年前

    一般来说,没有这种方法。不过,还有一些解决办法。通过拥有一个全局指针并在main/winmain中初始化/销毁它,您可以得到一个具有全局作用域且生存期略短于全局的对象。此外,还可以将全局状态放在引用计数堆对象的最后一个要销毁的位置。

    另外,考虑重新设计:)

        3
  •  1
  •   stusmith    14 年前

    "Modern C++ Design" 很好地解决了这个问题。

    谷歌图书包含了大部分内容的扫描——见第6.5节(第135页) link .

        4
  •  1
  •   EvilTeach    14 年前

    您可以通过在全局空间中放置指向对象的指针来清晰地处理这个问题, 然后在你的主中按所需的顺序更新它们,在主的末尾按所需的顺序销毁它们。

        5
  •  0
  •   mloskot    14 年前

    正如其他人指出的,由于 Static initialization order fiasco 问题。

    但是,您应该能够通过应用一点设计来解决您的问题,因此您可以在构建A和B的对象(以及如何构建)时获得一定程度的控制。看看设计模式,比如创意模式 Singleton 在许多情况下(如果不是大多数情况下),它被认为是 anti-pattern 尽管值得学习。也要看看 Monostate 可以作为一个更好的单件使用的模式。这些模式有助于控制对象的创建和生存期,因此在使用之前,可以对事物进行适当的初始化。

    一般来说,避免全球变暖是个好主意——坚持 deglobalisation 是个好主意。

        6
  •  0
  •   Pavel P    6 年前

    如果您使用懒惰的单例(返回按需创建的静态信息),那么可能会导致一个单例在删除后使用另一个单例。例如,假设您有一个全局 HttpClient 允许您发出HTTP请求的单例。另外,您可能希望有日志记录,日志记录可能由 Log singleton:

    class HttpClient
    {
        ...
        static HttpClient& singleton()
        {
            static HttpClient http;
            return http;
        }
    };
    

    同样 原木 独生子女。现在,想象一下 http客户端 构造函数和析构函数只需记录 HTTP客户端 已创建和删除。在这种情况下,的析构函数 HTTP客户端 可能最终会使用“已删除” 原木 独生子女。

    Sample code 以下内容:

    #include <stdio.h>
    
    class Log
    {
        Log()
        {
            msg("Log");
        }
    
        ~Log()
        {
            msg("~Log");
        }
    
    public:    
        static Log& singleton()
        {
            static Log log;
            return log;
        }
    
        void msg(const char* str)
        {
            puts(str);
        }
    };
    
    class HttpClient
    {
        HttpClient()
        {
            Log::singleton().msg("HttpClient");
        }
        ~HttpClient()
        {
            Log::singleton().msg("~HttpClient");
        }
    
    public:
        static HttpClient& singleton()
        {
            static HttpClient http;
            return http;
        }
        void request()
        {
            Log::singleton().msg("HttpClient::request");
        }
    };
    
    int main()
    {
        HttpClient::singleton().request();
    }
    

    输出为:

     Log
     HttpClient
     HttpClient::request
     ~HttpClient
     ~Log
    

    到目前为止,一切都是正确的,只是因为它发生了 原木 以前建造过 HTTP客户端 也就是说 HTTP客户端 仍然可以使用 原木 在它的析构函数中。现在只需注释掉登录代码 HTTP客户端 建设者和你最终会 this output :

    Log
    HttpClient::request
    ~Log
    ~HttpClient
    

    如您所见,日志在其析构函数之后被使用 ~Log 已被调用。正如所指出的,去全球化可能是一种更好的方法,但是如果您希望使用按需创建的单例,并使其中一些单例的寿命比其他单例的寿命长,那么您可以创建这样的单例 use a global static initialized on demand . 我经常在生产代码中使用这种方法:

    class Log
    {
        friend std::unique_ptr<Log>::deleter_type;
        ...
        static std::unique_ptr<Log> log;
    
        static Log& createSingleton()
        {
            assert(!log);
            log.reset(new Log);
            return *log;
        }
    
    public:    
        static Log& singleton()
        {
            static Log& log = createSingleton();
            return log;
        }
    };
    
    std::unique_ptr<Log> Log::log;
    

    现在,不管这些单体的建造顺序如何,破坏令将确保 原木 在之后被破坏 HTTP客户端 . 但是,如果 HTTP客户端 从全局静态构造函数使用。或者如果你想拥有多个这样的“超级”全局(比如 日志 Config 例如)如果以随机顺序使用彼此,您仍然会遇到这些问题。在这种情况下,有时候最好在堆上分配一次,不要删除其中的一些对象。