带shared_ptr的C++成员析构函数顺序

Ziq*_*Liu 1 c++ shared-ptr c++11

如果 classA包含 class B,那么在A析构时,B会先调用 \ 的析构函数,即它们的嵌套关系的顺序相反。

\n\n

但是,如果A包含一个shared_ptrof B,而B包含一个指向 的原始指针A,我们应该如何处理析构函数以使其安全?

\n\n

考虑以下示例:

\n\n
#include <iostream>\n#include <memory>\n#include <unistd.h>\n\nstruct B;\nstruct A {\n  int i = 1;\n  std::shared_ptr<B> b;\n\n  A() : b(std::make_shared<B>(this)) {}\n\n  ~A() {\n    b = nullptr;\n    std::cout << "A destruct done" << std::endl;\n  }\n};\n\nstruct B {\n  A *a;\n\n  B(A *aa) : a(aa) {}\n\n  ~B() {\n    usleep(2000000);\n    std::cout << "value in A: " << a->i << std::endl;\n    std::cout << "B destruct done" << std::endl;\n  }\n};\n\nint main() {\n  std::cout << "Hello, World!" << std::endl;\n  {\n    A a;\n  }\n  std::cout << "done\\n";\n  return 0;\n}\n\n
Run Code Online (Sandbox Code Playgroud)\n\n

A你可以在\的析构函数中看到,我显式地将b设置为nullptr,这将立即触发B\的析构函数,并阻塞直到它完成。\n输出将为\xef\xbc\x9a

\n\n
Hello, World!\nvalue in A: 1\nB destruct done\nA destruct done\ndone\n
Run Code Online (Sandbox Code Playgroud)\n\n

但如果我注释掉那行

\n\n
  ~A() {\n    // b = nullptr; // <---\n    std::cout << "A destruct done" << std::endl;\n  }\n
Run Code Online (Sandbox Code Playgroud)\n\n

输出将是:

\n\n
Hello, World!\nA destruct done\nvalue in A: 1\nB destruct done\ndone\n\n
Run Code Online (Sandbox Code Playgroud)\n\n

看来A\ 的析构函数无需等待B析构就完成了。但在这种情况下,我预期段错误,因为当A已经被破坏时,B尝试访问 的成员A,这是无效的。但为什么程序不会产生段错误呢?碰巧没问题吗(即,undefined behavior)?

\n\n

另外,当我改变时

\n\n
 {\n    A a;\n }\n
Run Code Online (Sandbox Code Playgroud)\n\n

\n\n
  A * a = new A();\n  delete a;\n
Run Code Online (Sandbox Code Playgroud)\n\n

输出仍然相同,没有段故障。

\n

Bri*_*ian 5

准确了解正在发生的事情非常重要。当A被销毁时,以下事件按以下顺序发生:

  • A::~A()叫做。
  • 对象A的生命周期结束。该对象仍然存在,但不再处于其生命周期内。([基本生活]/1.3)
  • 的主体A::~A()被执行。
  • A::~A()A以相反的声明顺序隐式调用直接非静态成员的析构函数([class.dtor]/9,[class.base.init]/13.3)
  • A::~A()返回。
  • A对象不再存在([class.dtor]/16)。它曾经占用的内存成为“已分配存储”([basic.life]/6),直到被释放为止。

(所有引用均参考 C++17 标准)。

在析构函数的第二个版本中:

~A() {
    std::cout << "A destruct done" << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

打印该语句后,该成员b被销毁,这会导致所拥有的B对象被销毁。此时,i尚未被销毁,因此可以安全地访问它。之后,B析构函数返回。然后,i被“销毁”(有关一些微妙之处,请参阅CWG 2256 )。最后,返回析构函数A。到那时,尝试访问该成员将不再合法i