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

C++ destructor order of member with shared_ptr

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

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

考虑以下示例:

#include <iostream>
#include <memory>
#include <unistd.h>

struct B;
struct A {
  int i = 1;
  std::shared_ptr<B> b;

  A() : b(std::make_shared<B>(this)) {}

  ~A() {
    b = nullptr;
    std::cout << "A destruct done" << std::endl;
  }
};

struct B {
  A *a;

  B(A *aa) : a(aa) {}

  ~B() {
    usleep(2000000);
    std::cout << "value in A: " << a->i << std::endl;
    std::cout << "B destruct done" << std::endl;
  }
};

int main() {
  std::cout << "Hello, World!" << std::endl;
  {
    A a;
  }
  std::cout << "done\n";
  return 0;
}

大家可以看到,在A的析构函数中,我明确的将b设置为nullptr,这会立即触发B的析构函数,并阻塞直到完成。 输出将是:

Hello, World!
value in A: 1
B destruct done
A destruct done
done

但是如果我注释掉那一行

  ~A() {
    // b = nullptr; // <---
    std::cout << "A destruct done" << std::endl;
  }

输出将是:

Hello, World!
A destruct done
value in A: 1
B destruct done
done

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

另外,当我改变

 {
    A a;
 }

  A * a = new A();
  delete a;

输出还是一样,没有段错误。

B 有一个指向 A 的指针,但没有释放它的内存(例如没有删除)。所以删除了指针但没有分配内存,这一切都很好。

基本上,指针在堆栈上,它包含堆上一些(假定的)已分配内存的地址。是的,它从堆栈中移除,但分配的内存仍然存在。这就是 delete 的用途。删除堆上分配的内存。但是,在您的情况下,您不希望删除该内存,而您的指针就是我们所说的非拥有指针。它指向某物,但不负责清理(实际上 B 不拥有指针指向的内存)。

If class A contains class B, then when A destruct, B's destructor will be called first, i.e., the reversed order of their nested relationship.

没有。如果您销毁类型为 A 的对象,它会调用 A 的析构函数,因此它会先被调用。

然而,调用 B 的析构函数将首先完成: A 的析构函数首先执行析构函数体,然后继续销毁子对象。析构函数体首先完成,然后是子对象的析构函数,最后 A 的析构函数将完成。

But what if A contains a shared_ptr of B, while B contains a raw pointer to A, how should we handle the destructor to make it safe?

A 的析构函数体中,将指向的 B 指向除被销毁对象之外的其他地方:

~A() {
    b->a = nullptr;
}

如果您将它指向 null,例如在我显示的示例中,那么您还必须确保 B 可以处理 B::a 可能为 null 的情况,即在通过指针访问之前进行检查.

it seems that A's destructor finished without waiting B to destruct.

这不是我们观察到的。 A 的析构函数的 body 已完成,但析构函数要等到成员析构函数先完成后才能完成。

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

  • A::~A() 被调用。
  • A 对象的生命周期结束。该对象仍然存在,但已不在其生命周期内。 ([basic.life]/1.3)
  • A::~A()的主体被执行。
  • A::~A() 以反向声明顺序隐式调用 A 的直接非静态成员的析构函数 ([class.dtor]/9, [class.base.init]/13.3 )
  • A::~A() returns.
  • A 对象不复存在 ([class.dtor]/16)。它曾经占用的内存变为 "allocated storage" ([basic.life]/6),直到它被释放。

(所有引用均针对 C++17 标准)。

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

~A() {
    std::cout << "A destruct done" << std::endl;
}

语句打印后,成员b被销毁,导致拥有的B对象被销毁。此时,i 尚未被销毁,因此可以安全访问它。之后,B析构函数returns。然后,i 是 "destroyed"(请参阅 CWG 2256 了解一些细微之处)。最后是Areturns的析构函数。到那时,尝试访问成员 i.

将不再合法

只是想指出您的评论不正确:

~A() {
  std::cout << "A destruct done" << std::endl;
}

当你离开花括号时,析构函数完成。您可以在调试器中看到,一步一步地进行。那就是 b 将被删除的地方。