C++中静态对象的销毁顺序问题

60

我能控制静态对象销毁的顺序吗?有没有办法强制指定我的期望顺序?比如说以某种方式指定某个对象最后被销毁,或者至少在另一个静态对象之后被销毁?

9个回答

64
静态对象的销毁顺序与构造顺序相反。而构造顺序很难控制。你唯一可以确定的是,在同一编译单元中定义的两个对象将按照定义顺序进行构造。其他任何情况都是比较随机的。

12
如果您需要一个顺序,有一些技巧可以解决这个问题:请参阅https://dev59.com/anRC5IYBdhLWcg3wUvUS。 - Martin York
3
我不同意这个回答,因为它并不真正回答了楼主的问题。 - user2746401
6
从技术上讲,“析构函数的完成”发生在“构造函数的完成”的相反顺序。这在静态对象在另一个构造函数期间初始化的情况下是有区别的。详见C++14 [basic.start.term]/1。 - M.M
如果可能的话,请回答我;如果我有静态对象'a'和'b'分别构造,现在如果'a'在'b'之前被析构,无论如何这可能发生。程序是否不合法? - mada

38

其他答案坚称这是不可能的。根据规范,他们是正确的 - 但是有一个技巧可以让您做到。

只需创建一个包含所有通常要创建为静态变量的内容(例如类或结构体)的单个静态变量即可,如下所示:

class StaticVariables {
    public:
    StaticVariables(): pvar1(new Var1Type), pvar2(new Var2Type) { };
    ~StaticVariables();

    Var1Type *pvar1;
    Var2Type *pvar2;
};

static StaticVariables svars;

StaticVariables的构造函数和析构函数中,您可以按任意顺序创建变量,并更重要的是按任意顺序销毁它们。为了使这完全透明,您还可以创建变量的静态引用,例如:

static Var1Type &var1(*svars.var1);

嗨,完全掌控了。 :-) 话虽如此,这需要额外的工作,通常是不必要的。但当它确实需要时,知道这一点非常有用。


1
嗯——如果pvar1和pvar2是您想要控制构造和销毁顺序的静态变量,我不禁要说,您通过将它们不再设置为静态变量来解决了这个问题;-)。实际上,您的类通常根本不需要:如果您可以在同一翻译单元中拥有所有变量(由于循环头文件依赖关系可能是不可能的),则可以简单地声明它们,这完全定义了构造(因此也是销毁)顺序。 - Peter - Reinstate Monica
只有在以下情况下,您的类伎俩才是必要且可行的:(1) 您的对象可以动态构建,并且不需要太多信息来构建(这通常仅在其“自然”TU中可用);(2) 如果所有所需的标头都可以共存;(3) 如果确实没有任何可能的构建顺序的反向符合您所需的销毁顺序。 - Peter - Reinstate Monica

14

静态对象的销毁顺序与构造顺序相反(例如第一个构造的对象最后被销毁),在Meyers的书《Effective C++》的第47项中描述了一种技术,可以控制静态对象的构造顺序,确保在使用之前已经初始化好全局对象。

例如,我如何指定某个对象在其他静态对象之后被销毁。

确保它在另一个静态对象之前被构造。

我如何控制静态对象的构造顺序?并非所有静态对象都在同一个dll中。

为简单起见,我会忽略它们不在同一个DLL中的事实。

根据Meyers的第47项(4页长),我的解释是这样的:假设你的全局变量定义在像这样的头文件中...

//GlobalA.h
extern GlobalA globalA; //declare a global

...在那个包含文件中添加一些代码,就像这样...

//GlobalA.h
extern GlobalA globalA; //declare a global
class InitA
{
  static int refCount;
public:
  InitA();
  ~InitA();
};
static InitA initA;
这将导致任何包含GlobalA.h文件的文件(例如,定义第二个全局变量的GlobalB.cpp源文件)都将定义一个InitA类的静态实例。这个实例将在该源文件中的其他任何东西之前被构造(例如,在第二个全局变量之前)。
这个InitA类有一个静态的引用计数器。当第一个InitA实例被构造时,现在可以保证它是在GlobalB实例被构造之前,InitA构造函数可以执行必须要做的所有操作,以确保globalA实例被初始化。

12

简短回答:一般来说,不行。

稍微详细的回答是:对于单个翻译单元中的全局静态对象,初始化顺序从上到下,销毁顺序完全相反。多个翻译单元之间的顺序是未定义的。

如果你真的需要特定的顺序,你需要自己构造。


尽管您的代码顺序通常是正确的,但我认为标准不要求顺序,因此它实际上取决于编译器。 - Robert Gould
对的,翻译单元之间的顺序是随机的。我说了别的吗? - gimpf
2
哥们!我写那个的时候真是困得不行啊!现在连我自己都不知道我当时想表达什么 :) - Robert Gould


5

在标准的C++中没有办法实现这个,但是如果你对你使用的特定编译器内部有很好的工作知识,那么可能可以实现。

在Visual C++中,指向静态初始化函数的指针位于.CRT$XI段(用于C类型静态初始化)或.CRT$XC段(用于C++类型静态初始化)。链接器收集所有声明并按字母顺序合并它们。通过在适当的段中声明对象,您可以控制静态初始化发生的顺序。

#pragma init_seg

例如,如果您想要在文件B之前创建文件A的对象:
文件A.cpp:
#pragma init_seg(".CRT$XCB")
class A{}A;

文件B.cpp:

#pragma init_seg(".CRT$XCC")
class B{}B;

.CRT$XCB会在.CRT$XCC之前合并。当CRT迭代静态初始化函数指针时,它会先遇到文件A再遇到文件B。

在Watcom中,该段是XI,变体的#pragma initialize可以控制构建:

#pragma initialize before library
#pragma initialize after library
#pragma initialize before user

...请查看文档以获取更多信息。


0

你真的需要在 main 函数之前初始化变量吗?

如果不需要,你可以使用一个简单的习惯用语来轻松地控制构造和析构的顺序,详情请看这里:

#include <cassert>

class single {
    static single* instance;

public:
    static single& get_instance() {
        assert(instance != 0);
        return *instance;
    }

    single()
    // :  normal constructor here
    {
        assert(instance == 0);
        instance = this;
    }

    ~single() {
        // normal destructor here
        instance = 0;
    }
};
single* single::instance = 0;

int real_main(int argc, char** argv) {
    //real program here...

    //everywhere you need
    single::get_instance();
    return 0;
}

int main(int argc, char** argv) {
    single a;
    // other classes made with the same pattern
    // since they are auto variables the order of construction
    // and destruction is well defined.
    return real_main(argc, argv);
}

这并不会阻止你尝试创建该类的第二个实例,但如果你这样做,断言将失败。根据我的经验,它可以正常工作。


0

不行,你不能这么做。你绝不能依赖于静态对象的构建和销毁顺序。

你可以使用单例来控制全局资源的构建和销毁顺序。


0

您可以通过使用static std::optional<T>而不是T来有效地实现类似的功能。只需像变量一样初始化它,使用间接引用并通过分配std::nullopt(或对于boost,boost::none)来销毁它。

与指针不同的是,它具有预分配的内存,这可能是您想要的。因此,如果您销毁它并(可能晚得多)重新创建它,则对象将具有相同的地址(您可以保留该地址),并且您不必在那时支付动态分配/释放的成本。

如果没有std:: / std::experimental::,请使用boost::optional<T>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接