如果类A
包含类B
,那么当A
析构时,将首先调用B
的析构函数,即它们嵌套关系的相反顺序。
但是,如果A
包含B
的shared_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
的析构函数将完成。
shared_ptr,而 B 包含指向 A 的原始指针,我们应该如何处理析构函数以使其安全?
在A
的析构函数主体中,将指向的B
指向要销毁的对象以外的其他位置:
~A() {
b->a = nullptr;
}
如果您将其指向 null,例如在我显示的示例中,那么您还必须确保B
可以处理B::a
可能为 null 的情况,即在通过指针访问之前进行检查。
似乎 A 的析构函数没有等待 B 销毁就完成了。
这不是我们观察到的。A
析构函数的主体已完成,但在成员析构函数首先完成之前,析构函数不会完成。