当调用析构函数或`delete`时,内存会被释放吗?

16

假设你有一个名为Fool的类的对象。

class Fool
{
    int a,b,c;
    double* array ;
    //...
    ~Fool()
    {
        // destroys the array..
        delete[] array ;
    }
};


Fool *fool = new Fool() ;

现在,我知道不应该这样做,但有些傻瓜仍然调用了fool上的析构函数。 fool->~Fool();

那么这意味着fool的内存被释放了(即a,b,c无效)还是只有~Fool()函数中的任何回收发生了(例如仅删除数组?)

所以我的问题是,析构函数只是在对象上调用delete时调用的另一个函数吗?还是它做得更多?


1
也许值得指出的是,你的类定义非常不完整;只有在某个时刻将new char[]的结果赋值给array,析构函数才可能有效。 - Kerrek SB
5个回答

32

如果你编写以下代码:

fool->~Fool();

你终止了对象的生命周期,这将调用析构函数并回收内部的array数组。但是,持有对象的内存不会被释放,这意味着如果你想使用定位new将对象重新唤醒:

new (fool) Fool;

你可以这样做。

根据规范,在显式调用析构函数后读取或写入fool字段的值将导致未定义的行为,因为对象的生命周期已经结束,但持有对象的内存仍应分配,并且您需要通过调用operator delete来释放它:

fool->~Fool();
operator delete(fool);

使用operator delete的原因是它比直接编写删除语句更安全。

delete fool;

后者的行为未定义,因为 fool 的生命周期已经结束。使用原始的释放函数 operator delete 可以确保内存被回收而不会尝试对对象的生命周期进行任何处理。

当然,如果对象的内存没有来自于 new (也许是栈分配的,或者你正在使用自定义的分配器),那么就不应该使用 operator delete 来释放它。如果这样做,你将再次面临未定义的行为。这似乎是这个问题中反复出现的主题。:-)

希望这能帮助到你!


2
我不确定你的描述是否完全对称。如果我说 char buf[20000]; Fool * f = new (buf+17) Fool;,那么我肯定不能调用 operator delete(f);。这就是整个问题的关键,分配与构造无关,只有获取内存的人才能决定如何处理它。 - Kerrek SB
1
@Kerrek 当我看到你的示例代码时,脑海中浮现了有关内存对齐的一些想法。 - Seth Carnegie
2
@Kerrek SB- 我不确定我理解你的最后一条评论。我说内部数组被回收,因为在析构函数中有一个delete[]的调用。我假设这个数组是正确分配的。我是否漏掉了重要的细节?还是我误解了你的最后一条评论? - templatetypedef
@Kerrek 这样有帮助吗?有人告诉我,栈上的 char 数组除了 char 以外没有对齐,可能是你告诉我的,我不记得了。char pad[3] 会有好坏参半的情况,不是吗? - Seth Carnegie
1
@Seth Carnegie- 哎呀...我是说new char[sizeof(Fool)]。谢谢你发现了这个错误! - templatetypedef
显示剩余7条评论

10

析构函数仅仅是调用了它本身,没有多余或者少于的操作。内存分配和对象构造是两个独立的过程,同样的,内存释放和对象析构也是两个独立的过程。

典型的执行顺序如下:

1. Allocate memory
2. Construct object
3. Destroy object  (assuming no exception during construction)
4. Deallocate memory

实际上,如果您手动运行此代码,您将需要自己调用析构函数:

void * addr = ::operator new(sizeof(Fool));
Fool * fp = new (addr) Fool;
fp->~Fool();
::operator delete(addr);

自动方式当然是这样写的 Fool * fp = new Fool; delete fp;new 表达式为您调用分配和构造,delete表达式调用析构函数并释放内存。


3
这是否意味着Fool的内存被释放了(即a、b、c无效),还是仅意味着在~Fool()函数中发生的任何解除分配(即仅删除数组)?Fool::~Fool()不知道Fool实例是存储在动态存储器(通过new)还是存储在自动存储器(即堆栈对象)中。由于对象在析构函数运行后将不再存在,因此您不能假设a、b、c和array在析构函数退出后仍然有效。但是,由于Fool::~Fool()不知道如何分配Fool,因此直接在新分配的Fool上调用析构函数不会释放支持对象的底层内存。

0

即使是显式析构函数调用,也不应该在析构函数被调用后访问abc,因为你永远不知道编译器会在析构函数中加入什么导致这些值无效。

然而,在显式析构函数调用的情况下,内存实际上并没有被释放。这是有意设计的;它允许使用放置new构造的对象进行清理。

例如:

char buf[sizeof (Fool)];
Fool* fool = new (buf) Fool;  // fool points to buf
// ...
fool->~Fool();

0

最容易看出析构函数与通过delete释放内存不同的地方是当分配本身是自动的时候:

{
  Fool fool;
  // ~Fool called on exit from block; nary a sign of new or delete
}

请注意,STL容器充分利用了显式析构函数调用。例如,std::vector<>会将存储和包含对象的生命周期完全分开处理。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接