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



如果类A包含类B,那么当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 << "donen";
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;

输出仍然相同,无段故障。

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

  • A::~A()被称为。
  • A对象的生存期结束。该对象仍然存在,但不再在其生存期内。([basic.life]/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;
}

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

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

基本上,指针位于堆栈上,它包含堆上某些(假定的(分配内存的地址。是的,它会从堆栈中删除,但分配的内存仍然存在。这就是delete的目的。删除堆上分配的内存。但是,在您的情况下,您不希望删除该内存,并且您的指针就是我们所说的非拥有指针。它指向某些东西,但它不负责清理(实际上 B 不拥有指针指向的内存(。

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

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

当您离开大括号时,析构函数已完成。您可以在调试器中看到这一点,逐步执行。这就是b将被删除的地方。

如果类 A 包含类 B,那么当 A 析构时,B 的析构函数将首先被调用,即它们的嵌套关系的相反顺序。

不。如果你销毁一个类型A的对象,则调用A的析构函数,因此首先调用该对象。

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

但是,如果 A 包含 B 的

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

A的析构函数主体中,将指向的B指向要销毁的对象以外的其他位置:

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

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

似乎 A 的析构函数没有等待 B 销毁就完成了。

这不是我们观察到的。A析构函数的主体已完成,但在成员析构函数首先完成之前,析构函数不会完成。

最新更新