Cub*_*bbi 50 c++ destructor standards-compliance
这个程序是否定义明确,如果没有,为什么呢?
#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;
}
Run Code Online (Sandbox Code Playgroud)
我的推理:虽然两次调用析构函数是未定义的行为,但按照12.4/14,它的确如此:
如果为生命周期结束的对象调用析构函数,则行为未定义
这似乎并没有禁止递归调用.当对象的析构函数正在执行时,对象的生命周期尚未结束,因此再次调用析构函数不是UB.另一方面,12.4/6说:
执行body [...]后,类X的析构函数调用X的直接成员的析构函数,X的直接基类的析构函数[...]
这意味着在从析构函数的递归调用返回之后,将调用所有成员和基类析构函数,并在返回到上一级递归时再次调用它们将是UB.因此,没有基数且只有POD成员的类可以具有不带UB的递归析构函数.我对吗?
Jam*_*lis 60
答案是否定的,因为§3.8/ 1中"生命周期"的定义:
类型对象的生命周期在以下情况
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;
}
Run Code Online (Sandbox Code Playgroud)
让我们运行它并在析构函数中设置一个断点,让递归的奇迹发生.
这是堆栈跟踪:
替代文字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
Run Code Online (Sandbox Code Playgroud)
在做我们的递归时,我们被困在地址上01341586,而内存实际上只在地址处释放01341597.
结论:在VS 2008中,由于析构函数只是一个方法,并且所有内存释放代码都被注入到中间函数(scalar deleting destructor)中,因此可以安全地递归调用析构函数.但IMO仍然不是个好主意.
编辑:好的,好的.这个答案的唯一想法是看看递归调用析构函数时发生了什么.但是不要这样做,一般不安全.
它回到编译器对对象生命周期的定义.就像在,什么时候内存真的被解除分配.我认为直到析构函数完成后才能进行,因为析构函数可以访问对象的数据.因此,我希望递归调用析构函数.
但是......确实有很多方法可以实现析构函数和释放内存.即使它在我今天使用的编译器上按照我的意愿工作,我也会非常谨慎地依赖这种行为.有很多东西,文档说它不起作用或结果是不可预测的,事实上,如果你了解内部真正发生的事情,工作就好了.但是依靠它们是不好的做法,除非你真的必须这样做,因为如果规范说这不起作用,那么即使它确实有效,你也无法保证它会继续在下一版本的编译器.
也就是说,如果你真的想以递归的方式调用你的析构函数,这不仅仅是一个假设的问题,为什么不只是将析构函数的整个主体撕成另一个函数,让析构函数调用它,然后让它自己调用呢?那应该是安全的.
| 归档时间: |
|
| 查看次数: |
6995 次 |
| 最近记录: |