这个程序是否定义明确,如果没有,为什么呢?
#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;
}
我的理由:虽然invoking a destructor twice is undefined behavior,按照12.4 / 14,它的确如此:
如果是,则行为未定义 为对象调用析构函数 其寿命已经结束
似乎没有禁止递归调用。当对象的析构函数正在执行时,对象的生命周期尚未结束,因此再次调用析构函数不是UB。另一方面,12.4 / 6表示:
执行身体后[...] a 类X的析构函数调用 X直接成员的析构函数, X的直接基础的析构函数 课程[...]
这意味着在从析构函数的递归调用返回之后,将调用所有成员和基类析构函数,并在返回到上一级递归时再次调用它们将是UB。因此,没有基数且只有POD成员的类可以具有不带UB的递归析构函数。我是对的吗?
答案 0 :(得分:58)
答案是否定的,因为§3.8/ 1中“生命周期”的定义:
类型
T
的对象的生命周期在以下时间结束:- 如果
T
是具有非平凡析构函数(12.4)的类类型,则析构函数调用开始,或- 重用或释放对象占用的存储空间。
一旦调用析构函数(第一次),对象的生命周期就结束了。因此,如果从析构函数中调用对象的析构函数,则行为未定义,符合§12.4/ 6:
如果为生命周期结束的对象调用析构函数
,则行为未定义
答案 1 :(得分:9)
好的,我们知道没有定义行为。但是,让我们进入真正发生的事情的小旅程。我使用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;
}
让我们运行它并在析构函数中设置一个断点,让递归的奇迹发生。
这是堆栈跟踪:
alt text http://img638.imageshack.us/img638/8508/dest.png
那是什么scalar deleting destructor
?这是编译器在delete和我们的实际代码之间插入的东西。析构函数本身只是一种方法,没有什么特别之处。它并没有真正释放内存。它在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仍然不是个好主意。
修改:好的,好的。这个答案的唯一想法是看看递归调用析构函数时发生了什么。但是不要这样做,一般都不安全。
答案 2 :(得分:5)
它回到编译器对对象生命周期的定义。就像在,什么时候内存真的被解除分配。我认为直到析构函数完成后才能进行,因为析构函数可以访问对象的数据。因此,我希望递归调用析构函数。
但是......肯定有很多方法可以实现析构函数和释放内存。即使它在我今天使用的编译器上按照我的意愿工作,我也会非常谨慎地依赖这种行为。有很多东西,文档说它不起作用或结果是不可预测的,事实上,如果你了解内部真正发生的事情,工作就好了。但是依靠它们是不好的做法,除非你真的必须这样做,因为如果规范说这不起作用,那么即使它确实有效,你也无法保证它会继续在下一版本的编译器。
那就是说,如果你真的想要递归地调用你的析构函数,这不只是一个假设的问题,为什么不只是将析构函数的整个主体撕成另一个函数,让析构函数调用它,然后让那个调用本身递归?这应该是安全的。
答案 3 :(得分:1)
是的,这听起来是正确的。我认为一旦析构函数完成调用,内存将被转储回可分配的池中,允许在其上写入内容,从而可能导致后续析构函数调用的问题('this'指针无效)。 / p>
但是,如果析构函数在递归循环解开之前没有完成,那理论上应该没问题。
有趣的问题:)
答案 4 :(得分:0)
为什么有人想以这种方式递归调用析构函数?一旦调用了析构函数,就应该销毁该对象。如果你再次调用它,那么当你实际上仍在同时实际销毁它时,你会试图破坏已经部分被破坏的物体。
所有示例都有某种递减/增量结束条件, 基本上在调用中倒计时,这表明嵌套类的某种失败实现,其中包含与其自身相同类型的成员。
对于这样一个嵌套的matryoshka类,以递归方式调用成员上的析构函数,即析构函数调用成员A上的析构函数,而成员A又在其自己的成员A上调用析构函数,后者又调用析构函数...等等非常好,并且完全按照人们的预期工作。这是析构函数的递归使用,但不以递归方式调用析构函数本身是疯狂的,并且几乎没有任何意义。