我一直在为我无法真正理解的析构函数调用顺序而苦苦挣扎。
假设我们有以下定义:
#include <memory>
#include <iostream>
class DummyClass {
std::string name;
public:
DummyClass(std::string name) : name(name) { std::cout << "DummyClass(" << name << ")" << std::endl; }
~DummyClass() { std::cout << "~DummyClass(" << name << ")" << std::endl; }
};
class TestClass {
private:
static DummyClass dummy;
static DummyClass& objects() {
static DummyClass dummy("inner");
return dummy;
}
public:
TestClass() {
std::cout << "TestClass" << std::endl;
std::cout << "TestClass Objects is: " << &objects() << std::endl;
}
virtual ~TestClass() {
std::cout << "~TestClass Objects is: " << &objects() << std::endl;
std::cout << "~TestClass" << std::endl;
}
};
DummyClass TestClass::dummy("outer");
现在,如果我按如下方式实例化TestClass
:
TestClass *mTest = nullptr;
int main() {
mTest = new TestClass(); delete mTest;
return 0;
}
获得的输出是我期望的:
DummyClass(outer)
TestClass
DummyClass(inner)
TestClass Objects is: 0x....
~TestClass Objects is: 0x....
~TestClass
~DummyClass(inner)
~DummyClass(outer)
但是,现在,如果我使用 mTest 的shared_ptr,例如:
std::shared_ptr<TestClass> mTest;
int main() {
mTest = std::make_shared<TestClass>();
return 0;
}
生成的输出为:
DummyClass(outer)
TestClass
DummyClass(inner)
TestClass Objects is: 0x....
~DummyClass(inner)
~TestClass Objects is: 0x....
~TestClass
~DummyClass(outer)
有人可以解释为什么在这种特殊情况下,DummyClass 内部对象在 TestClass 对象析构函数结束之前被销毁吗? 我发现使用 -std=gnu++11 的 gcc 5.2.0 和 -std=c++11 的 clang 3.8.0 的行为一致,但找不到任何引用此示例的特定文档。
编辑:澄清一下:上面的所有代码都是按照呈现的顺序在同一个翻译单元(*.cpp文件)中编写的。这是对用例的简化,其中我有一个仅标头类定义,该类定义必须包含指向派生类对象的this
指针的静态列表。这些指针通过 ctor 添加,并在到达 dtor 时删除。销毁最后一个对象时会触发此问题。该列表保存在静态方法中,并通过它访问以实现仅标头目标。
具有静态存储持续时间的所有对象(命名空间成员、静态类成员和函数定义中的static
对象)的规则为:
-
如果整个初始化可以被视为常量表达式,则该初始化先于其他任何事情发生。(不适用于示例中的任何内容。否则
-
命名空间成员和静态类成员保证在调用同一翻译单元中的任何函数之前的某个时间点开始初始化。 (在大多数实现中,如果我们忽略动态库加载,所有这些都发生在
main
开始之前。 在您的示例中,由于main
位于同一 TU 中,我们知道它们发生在main
.) -
在同一 TU 中定义的命名空间成员和静态类成员按其定义的顺序开始初始化。
-
对于在不同 TU 中定义的命名空间成员和静态类成员,无法保证初始化顺序!
-
函数中定义的静态对象在程序控件第一次到达定义时开始初始化(如果有的话)。
-
当调用
main
返回或调用std::exit
时,所有具有静态存储持续时间的对象都将按与每个对象完成初始化时相反的顺序销毁。
所以在你的第二个例子中:
-
开始初始化
TestClass::dummy
。首先创建一个临时std::string
,然后调用DummyClass::DummyClass(std::string)
。 -
DummyClass
构造函数执行std::string
复制,然后输出"DummyClass(outer)n"
。临时std::string
被销毁。TestClass::dummy
初始化完成。 -
开始初始化
::mTest
。这称为std::shared_ptr<TestClass>::shared_ptr()
. -
构造
shared_ptr
将智能指针设置为 null。::mTest
的初始化已完成。 -
main
开始了。 -
std::make_shared
调用最终会创建一个TestClass
对象,调用TestClass::TestClass()
。这个构造函数首先打印"TestClassn"
,然后调用TestClass::objects()
。 -
在
TestClass::objects()
内部,本地对象dummy
的初始化开始。再次创建一个临时std::string
,并调用DummyClass::DummyClass(std::string)
。 -
DummyClass
构造函数执行std::string
复制,然后输出"DummyClass(inner)n"
.临时std::string
被销毁。objects
'dummy
的初始化已完成。 -
TestClass::TestClass()
继续,打印"TestClass Objects is: 0x
...n"
. 动态TestClass
对象的初始化已完成。 -
回到
main
,make_shared
函数返回一个临时std::shared_ptr<TestClass>
。移动分配从返回的临时移动到::mTest
,然后临时被销毁。请注意,尽管TestClass
对象与::mTest
相关联,但它具有动态存储持续时间,而不是静态存储持续时间,因此上述规则不适用于它。 -
main
回归。C++开始销毁具有静态存储持续时间的对象。 -
最后一个完成初始化的静态对象是上面步骤 8 中
TestClass::objects()
的dummy
本地对象,因此首先销毁它。其析构体输出"~DummyClass(inner)n"
. -
在上面的步骤 4 中
::mTest
下一个要完成初始化的对象,因此接下来开始销毁它。~shared_ptr
析构函数最终会销毁拥有的动态TestClass
对象。 -
TestClass::~TestClass()
析构函数主体首先调用TestClass::objects()
。 -
在
TestClass::objects()
中,我们遇到了一个已经被破坏的函数局部静态的定义,即未定义的行为! 显然,您的实现除了返回对以前包含dummy
的存储的引用外什么都不做,除了获取地址之外,您没有对它做任何事情可能是一件好事。 -
TestClass::~TestClass()
继续,输出"~TestClass Objects is: 0x
...n"
然后"~TestClassn"
. -
::mTest
的~shared_ptr
析构函数解除分配关联的内存并完成。 -
最后,在上面的步骤 2 中
TestClass::dummy
第一个完成初始化的静态对象,因此最后销毁了它。DummyClass::~DummyClass
析构函数主体输出"~DummyClassn"
。程序已完成。
因此,您的两个示例之间的最大区别在于,TestClass
销毁会延迟到shared_ptr
被销毁 - 在事物方案中创建TestClass
的时间并不重要。由于shared_ptr
是在第二个示例中的"内部"DummyClass
之前创建的,因此它的销毁发生在"内部"对象消失之后,从而导致未定义的行为。
如果这是您遇到并需要修复的实际问题的简化,您可以尝试添加类似
class TestClass {
// ...
public:
class ForceInit {
ForceInit() { TestClass::objects(); }
};
// ...
};
// ...
TestClass::ForceInit force_init_before_mTest;
std::shared_ptr<TestClass> mTest;
它与shared_ptr无关,而是与模块(cc 文件)中全局变量的销毁顺序有关。规范指出顺序是未定义的,因此您不能假设静态内部对象将在另一个全局对象之后或之前被销毁。如果您需要一致的销毁顺序,我建议您明确处理它。