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

时间:2010-01-29 21:17:37

标签: c++ memory-management global-variables destructor

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

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

如果A对象在B中的静态成员之前构造或在B中的静态成员之后被破坏,则会在某些时候导致崩溃(通常是访问冲突)。

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

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

6 个答案:

答案 0 :(得分:8)

没有。这被称为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_Aget_B来获取全局实例。 B使用A的部分应使用get_AB的使用应与get_B一致。请注意,get_B在您的情况下是可选的。

首次创建B时会发生什么? (全局或函数中)构造函数将调用get_A ,其中将创建A。这让你控制事物的构造顺序。

注意我想我颠倒了你的A和B.

答案 1 :(得分:1)

一般来说,没有这样的方法。但是有一些解决方法。通过使用全局指针并在main / WinMain中初始化/破坏它,可以获得具有全局范围和略小于全局生命周期的对象。此外,您将全局状态放在最后一个引用计数的堆对象中。

另外,考虑重新设计:)

答案 2 :(得分:1)

这本书"Modern C++ Design"很好地涵盖了这个问题。

Google图书包含大部分扫描内容 - 请参阅第6.5节(第135页) - link

答案 3 :(得分:1)

你可以通过在全局空间中指向对象来干净利落地处理这个问题, 然后在你的主要部分以所需的顺序新建它们,并在主要结束时以所需的顺序销毁它们。

答案 4 :(得分:0)

答案 5 :(得分:0)

如果使用懒惰的单例(返回按需创建的静态变量),则可能会导致一个单例在已被删除后使用另一单例的可能性。例如,假设您有一个全局HttpClient单例,该单例允许您发出http请求。另外,您可能希望记录日志,该记录可能由Log单身人士提供:

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

Log单身人士相同。现在,假设HttpClient的构造函数和析构函数只是记录了HttpClient的创建和删除。在这种情况下,HttpClient的析构函数可能最终会使用已删除的Log单例。

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

到目前为止,所有内容都是正确的,仅仅是因为发生Log是在HttpClient之前构造的,这意味着HttpClient仍可以在其析构函数中使用Log。现在,只需注释掉HttpClient构造函数中的日志记录代码,您将得到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;

现在,无论构造这些单例的顺序如何,销毁顺序都将确保LogHttpClient之后被销毁。但是,如果从全局静态构造函数使用HttpClient,这可能仍然会失败并产生意外的输出。或者,如果您希望有多个这样的“超级”全局变量(例如LogConfig)以随机顺序相互使用,那么您仍然会遇到这些问题。在这种情况下,最好只在堆上分配一次,永远不要删除其中一些对象。