如何正确释放由placement new分配的内存?

33

我曾在某处读到过,当你使用定位new时,必须手动调用析构函数。

考虑以下代码:

   // Allocate memory ourself
char* pMemory = new char[ sizeof(MyClass)];

// Construct the object ourself
MyClass* pMyClass = new( pMemory ) MyClass();

// The destruction of object is our duty.
pMyClass->~MyClass();
据我所知,操作符delete通常会调用析构函数,然后释放内存,对吗?那么为什么我们不使用delete呢?
答案:据我所知,操作符delete通常会调用析构函数,然后释放内存。那为什么不直接使用delete呢?
delete pMyClass;  //what's wrong with that?

在第一种情况下,我们在调用析构函数后被迫将 pMyClass 设置为 nullptr,就像这样:

pMyClass->~MyClass();
pMyClass = nullptr;  // is that correct?

但是析构函数并没有释放内存,对吗? 那这算不算内存泄漏呢?

我有些困惑,你能解释一下吗?


12
从技术上讲,placement new 不会分配内存。但在这种情况下,这只是一个细节。 - Griwes
1
@Griwes:+1。可以说这是一个基本观点 :) - Stuart Golodetz
5个回答

45
使用 new 表达式会做两件事情,首先它调用函数 operator new 来分配内存,然后使用放置 new,在该内存中创建对象。而 delete 表达式则会调用对象的析构函数,然后调用 operator delete。是的,这些名称确实有些令人困惑。
//normal version                   calls these two functions
MyClass* pMemory = new MyClass;    void* pMemory = operator new(sizeof(MyClass));
                                   MyClass* pMyClass = new( pMemory ) MyClass();
//normal version                   calls these two functions
delete pMemory;                    pMyClass->~MyClass();
                                   operator delete(pMemory);

在您的情况下,您手动使用了放置new操作符,因此您还需要手动调用析构函数。由于您手动分配了内存,因此需要手动释放它。

但是,放置new操作符也设计用于使用内部缓冲区(以及其他场景),其中缓冲区不是使用operator new分配的,这就是为什么您不应该对其调用operator delete的原因。

#include <type_traits>

struct buffer_struct {
    std::aligned_storage_t<sizeof(MyClass), alignof(MyClass)> buffer;
};
int main() {
    buffer_struct a;
    MyClass* pMyClass = new (&a.buffer) MyClass(); //created inside buffer_struct a
    //stuff
    pMyClass->~MyClass(); //can't use delete, because there's no `new`.
    return 0;
}
< p > buffer_struct 类的目的是以任何方式创建和销毁存储空间,而 main 则负责构造/销毁 MyClass,请注意它们(几乎)完全独立于彼此。

* 我们必须确保存储空间足够大


3
这是问题的根源:“但是,placement new是为内部缓冲区设计的,它们本身没有使用new进行分配,这就是为什么你不应该调用delete释放它们的原因。”你不释放内存,因为在将来某个时候,您将使用该缓冲区的一部分来创建同一类型的其他对象。您调用析构函数以便实例可以进行清理。 - That Chuck Guy
不错的例子。因此,要删除缓冲区,我们调用:delete[] a.buffer(如果它是动态的),那么我们如何调用MyClass的析构函数呢? - codekiddy
@codekiddy:由于 a.buffer 不是动态分配的,因此不需要释放。在我的示例中,它是自动分配的,因此缓冲区在各个方面都是完全自动的。唯一需要考虑的是类的放置 new 和调用该类的析构函数,就像我的示例代码所示。 - Mooing Duck
1
缓冲区很可能是使用new分配的 - 使用放置new的重点是将内存分配与初始化分开。您以某种方式分配内存(无论是否使用new),然后将对象构造到分配的内存中。这就是为什么您不应该尝试同时销毁对象和释放内存 - 整个重点是将内存分配和对象生命周期问题分开(而且通常不起作用)。 - Stuart Golodetz
3
顺便提一下,C++11 中有 max_align_t 这个类型,它具有最大的对齐方式。还有 std::aligned_storage。不过 Fred 所说的仍然成立。这里没有对齐保证。 - R. Martinho Fernandes
@MooingDuck Fred说你的缓冲区没有提供任何对齐保证。它可能足够大,让你在其中对齐数据,但你实际上并没有对齐它。 - R. Martinho Fernandes

10

这种做法是错误的原因之一:

delete pMyClass;

这意味着您必须使用delete[]删除pMemory,因为它是一个数组:

delete[] pMemory;

你不能同时执行上述两个操作。

同样地,你可能会问为什么不能使用malloc()来分配内存,使用放置new来构造对象,然后使用delete来删除并释放内存。原因是你必须匹配malloc()free(),而不是malloc()delete

在现实世界中,放置new和显式析构函数调用几乎从未被使用。它们可能被标准库实现(或其他系统级编程中提到的注释)内部使用,但普通程序员不使用它们。我在多年的C++编程中从未在生产代码中使用过这样的技巧。


1
只是提一下:在C++中进行操作系统开发或类似工作时,放置new也经常被使用。 - Griwes
嗨,实际上我们不必手动调用析构函数,因为delete[] pMemory将会调用析构函数并释放内存,对吧? - codekiddy
2
@codekiddy:delete[] pMemory 不会调用您对象的析构函数。 - Greg Hewgill
4
当你说 delete[] x; 时,它会调用 x 数组中每个元素的析构函数。由于你的数组是 char 类型,所以对于每个字符不做任何操作。你之前只是碰巧在那里构造和析构了一个 MyClass,但这与 newdelete 没有任何关系。 - GManNickG

5
你需要区分delete操作符和operator delete。特别是,如果你正在使用放置new,你需要显式调用析构函数,然后调用operator delete(而不是delete操作符)来释放内存,即:
X *x = static_cast<X*>(::operator new(sizeof(X)));
new(x) X;
x->~X();
::operator delete(x);

请注意,这里使用的是operator delete,它比delete运算符更低级,并且不涉及析构函数(基本上有点像free)。与之相比,delete运算符会在内部执行调用析构函数和调用operator delete等价的操作。
值得注意的是,您不必使用::operator new::operator delete来分配和释放缓冲区——就放置new而言,缓冲区如何产生/被销毁并不重要。主要的核心思想是将内存分配和对象生命周期分离开来。
顺便提一下,可能会在类似游戏中应用它,在那里您可能希望预先分配大块内存以便精细地管理内存使用情况。然后,您可以在已经获得的内存中构建对象。
另一个可能的用途是优化的小型、固定大小的对象分配器。

3
如果您想象在一块内存中构建几个 MyClass 对象,那么理解起来可能更容易些。
在这种情况下,大致如下:
1. 使用 new char[10*sizeof(MyClass)] 或 malloc(10*sizeof(MyClass)) 分配一块巨大的内存块。 2. 使用 Placement New 在该内存中构造十个 MyClass 对象。 3. 做一些操作。 4. 调用每个对象的析构函数。 5. 使用 delete[] 或 free() 释放整个内存块。
如果您正在编写编译器或操作系统等代码,则可能需要执行此类操作。
在这种情况下,我希望您清楚为什么需要分开“析构函数”和“删除”步骤,因为您不一定会调用 delete。但是,您应该按照通常的方式(free、delete、对于巨大静态数组什么都不做、如果数组是另一个对象的一部分则正常退出等)释放内存;否则,内存将无法释放。
同时,请注意,如 Greg 所说,在这种情况下,您不能使用 delete,因为您使用 new[] 分配了数组,所以必须使用 delete[]。
还要注意,您需要假设未覆盖 MyClass 的 delete 函数,否则它将执行与“new”几乎完全不兼容的其他操作。
因此,我认为您不太可能想要像您描述的那样调用“delete”,但它有可能起作用吗?我认为这基本上与“我有两个不相关的类型,它们没有析构函数。我可以使用一个类型的指针 new 一个指针,然后通过另一种类型的指针删除该内存吗?”这个问题相同。换句话说,“我的编译器是否拥有所有已分配内容的一个大列表,或者可以为不同的类型执行不同的操作”。
我不确定。读取规范,它说:
5.3.5 ... 如果操作数的静态类型与其动态类型不同,则静态类型必须是操作数动态类型的基类,并且静态类型必须具有虚拟析构函数,否则行为未定义。
我认为这意味着:“如果您使用两个不相关的类型,则不起作用(可以通过虚拟析构函数多态地删除类对象)”。
因此,请不要这样做。我认为在实践中它可能经常起作用,如果编译器仅查看地址而不查看类型(并且两种类型都不是多重继承类,否则将破坏地址),但是不要尝试。

0

您将使用放置 new 进行共享内存 IPC:一个“初始化器”进程保留并映射共享内存,然后映射的内存由所有进程共享。


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