析构函数可以递归吗?

时间:2010-06-17 15:55:05

标签: 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;
}

我的理由:虽然invoking a destructor twice is undefined behavior,按照12.4 / 14,它的确如此:

  

如果是,则行为未定义   为对象调用析构函数   其寿命已经结束

似乎没有禁止递归调用。当对象的析构函数正在执行时,对象的生命周期尚未结束,因此再次调用析构函数不是UB。另一方面,12.4 / 6表示:

  

执行身体后[...] a   类X的析构函数调用   X直接成员的析构函数,   X的直接基础的析构函数   课程[...]

这意味着在从析构函数的递归调用返回之后,将调用所有成员和基类析构函数,并在返回到上一级递归时再次调用它们将是UB。因此,没有基数且只有POD成员的类可以具有不带UB的递归析构函数。我是对的吗?

5 个答案:

答案 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上调用析构函数,后者又调用析构函数...等等非常好,并且完全按照人们的预期工作。这是析构函数的递归使用,但以递归方式调用析构函数本身是疯狂的,并且几乎没有任何意义。

相关问题