一个析构函数可以递归吗?

这个程序是否定义明确,如果不是,为什么?

#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?