一个析构函数可以递归吗?
这个程序是否定义明确,如果不是,为什么?
#include <iostream>
#include <new>
struct X {
int cnt;
X (int i) : cnt(i) {}
~X() {
std::cout << "destructor called, cnt=" << cnt << std::endl;
if ( cnt-- > 0 )
this->X::~X(); // explicit recursive call to dtor
}
};
int main()
{
char* buf = new char[sizeof(X)];
X* p = new(buf) X(7);
p->X::~X(); // explicit call to dtor
delete[] buf;
}
我的推理:尽管调用析构函数两次是未定义行为,按照12.4 / 14,它所说的究竟是这样的:
如果析构函数针对其生存期已结束的对象调用,则行为未定义
这似乎不禁止递归调用。 虽然对象的析构函数正在执行,但对象的生命周期还没有结束,因此再次调用析构函数并不是UB。 另一方面,12.4 / 6说:
在执行完本体后,X类的析构函数调用X的直接成员的析构函数,X的直接基类的析构函数[...]
这意味着在从递归调用析构函数返回之后,所有成员和基类析构函数将被调用,并且在返回到先前的递归级别时再次调用它们将是UB。 因此,没有基础和只有POD成员的类可以有没有UB的递归析构函数。 我对吗?
答案是否定的,因为在§3.8/ 1中定义了“lifetime”
类型T
的对象的生命周期在以下情况下结束:
- 如果T
是一个具有非平凡析构函数的类类型(12.4),则析构函数调用开始,或者
- 物体占用的存储空间被重新使用或释放。
只要调用析构函数(第一次),对象的生命周期就结束了。 因此,如果从析构函数中调用该对象的析构函数,则根据§12.4/ 6,行为是未定义的:
如果析构函数针对其生存期已结束的对象调用,则行为未定义
好的,我们知道行为没有定义。 但是,让我们做一些真正发生的事情。 我使用VS 2008。
这是我的代码:
class Test
{
int i;
public:
Test() : i(3) { }
~Test()
{
if (!i)
return;
printf("%d", i);
i--;
Test::~Test();
}
};
int _tmain(int argc, _TCHAR* argv[])
{
delete new Test();
return 0;
}
让我们运行它并在析构函数内设置一个断点,让递归的奇迹发生。
这是堆栈跟踪:
替代文字http://img638.imageshack.us/img638/8508/dest.png
这个scalar deleting destructor
什么? 这是编译器在删除和实际代码之间插入的东西。 析构函数本身只是一种方法,没有什么特别的。 它并没有真正释放内存。 它在scalar deleting destructor
某处被释放。
让我们去scalar deleting destructor
并看看反汇编:
01341580 mov dword ptr [ebp-8],ecx
01341583 mov ecx,dword ptr [this]
01341586 call Test::~Test (134105Fh)
0134158B mov eax,dword ptr [ebp+8]
0134158E and eax,1
01341591 je Test::`scalar deleting destructor'+3Fh (134159Fh)
01341593 mov eax,dword ptr [this]
01341596 push eax
01341597 call operator delete (1341096h)
0134159C add esp,4
在进行递归时,我们被卡在地址01341586
,而内存实际上只在地址01341597
处被释放。
结论:在VS 2008中,由于析构函数只是一种方法,所有的内存释放代码都被注入到中间函数( scalar deleting destructor
)中,所以递归调用析构函数是安全的。 但是,IMO仍然不是个好主意。
编辑 :好的,好的。 这个答案的唯一想法是看看你在递归调用析构函数时发生了什么。 但不这样做,一般不安全。
它回到了编译器对对象生命周期的定义。 就像在什么时候,内存真的被解除分配一样。 我认为它不能在析构函数完成之后,因为析构函数可以访问对象的数据。 因此,我希望递归调用析构函数来工作。
但是......确实有很多方法来实现析构函数和释放内存。 即使它在我今天使用的编译器上按照我想要的方式工作,我仍然会非常谨慎地依赖这种行为。 有很多文件说文件不起作用,或者结果是不可预测的,事实上,如果你了解内部真正发生的事情,它就可以很好地工作。 但除非你真的必须依赖它们,否则依赖它们是不好的做法,因为如果规范说这不起作用,那么即使它确实有效,也不能保证它会继续在下一版本的编译器。
也就是说,如果你真的想递归地调用你的析构函数,这不仅仅是一个假设问题,为什么不只是把析构函数的整个体翻译成另一个函数,让析构函数调用它,然后递归地调用它自己呢? 这应该是安全的。
链接地址: http://www.djcxy.com/p/82809.html上一篇: Can a destructor be recursive?
下一篇: Does calling a destructor explicitly destroy an object completely?