“placement new” 有哪些用途?

515

这里有人使用过C++的“定位new”吗?如果使用过,是用来做什么的?在我看来,它似乎只对内存映射硬件有用。


21
这正是我一直在寻找的信息,可以在boost分配的内存池上调用对象构造函数。(希望这些关键词能够方便将来的某个人查找到它)。 - Sideshow Bob
3
它被用于C++11维基百科文章中,作为一个联合体的构造函数。 - HelloGoodbye
1
@HelloGoodbye,很有趣!在您所链接的文章中,为什么不能只做 p = pt 并使用 Point 的赋值运算符,而要做 new(&p) Point(pt)?我想知道两者之间的区别。前者会调用 Pointoperator=,而后者会调用 Point 的复制构造函数吗?但我还不是很清楚为什么一个比另一个更好。 - Andrei-Niculae Petre
1
@Andrei-NiculaePetre 我自己没有使用过放置 new,但我猜想如果你当前没有该类的对象,你应该使用它——连同复制构造函数一起使用,否则你应该使用复制赋值运算符。除非该类是平凡的;否则无论你使用哪个都无所谓。同样的事情也适用于对象的销毁。如果对于非平凡的类不能正确处理这个问题,很可能会导致奇怪的行为,甚至在某些情况下可能会导致未定义的行为。 - HelloGoodbye
1
这里有一些优秀的使用演示/示例,以说明如何使用放置new并展示其功能!https://www.geeksforgeeks.org/placement-new-operator-cpp/ - Gabriel Staples
显示剩余6条评论
25个回答

465

Placement new允许您在已经分配的内存中构造对象。

当您需要构造多个对象实例时,为了优化,您可能希望这样做,并且每次需要新对象实例时重新分配内存会更加耗时。相反,执行一次分配来获取可以容纳多个对象的内存块可能更加高效,即使您并不想一次性使用全部内存。

DevX给出了一个良好的示例:

标准C++还支持placement new运算符,它在预分配的缓冲区上构造对象。当构建内存池、垃圾回收器或者性能和异常安全至关重要时(由于内存已经被分配,不存在分配失败的风险,而在预分配的缓冲区上构造对象所需时间更少),此方法非常有用:

char *buf  = new char[sizeof(string)]; // pre-allocated buffer
string *p = new (buf) string("hi");    // placement new
string *q = new string("hi");          // ordinary heap allocation

如果您希望在临界代码的某个部分(例如由起搏器执行的代码)确保没有分配失败,那么您需要提前分配内存,然后在关键部分使用就地构造 (placement new)。

就地构造中的释放

您不应该释放使用内存缓冲区的每个对象。相反,您应该仅删除原始缓冲区(delete[] original buffer),然后手动调用类的析构函数。有关此内容的良好建议,请参见 Stroustrup 的 FAQ: Is there a "placement delete"?


60
如果要高效实现容器对象(如vector),就需要使用这个功能,所以它并没有被废弃。但是如果你不是在构建自己的容器,就不需要使用这个功能。 - Martin York
30
同样重要的是记得 #include <memory>,否则在一些不自动识别放置 new 的平台上可能会遇到一些可怕的头痛问题。 - Ramon Zarazua B.
26
严格来说,在原始 char 缓冲区上调用 delete[] 是未定义的行为。使用放置 new 已经通过重用它们的存储结束了原始 char 对象的生命周期。如果现在调用 delete[] buf,则对象指向的动态类型不再匹配其静态类型,因此会产生未定义的行为。更一致的做法是使用 operator new / operator delete 来分配原始内存,供放置 new 使用。 - CB Bailey
37
我肯定会避免在起搏器中使用堆(heap) :-) - Eli Bendersky
18
@RamonZarazua 错误的头文件,应该是 #include <new> - bit2shift
显示剩余13条评论

75

我们将其与自定义内存池一起使用。只是一个简单的草图:

class Pool {
public:
    Pool() { /* implementation details irrelevant */ };
    virtual ~Pool() { /* ditto */ };

    virtual void *allocate(size_t);
    virtual void deallocate(void *);

    static Pool *Pool::misc_pool() { return misc_pool_p; /* global MiscPool for general use */ }
};

class ClusterPool : public Pool { /* ... */ };
class FastPool : public Pool { /* ... */ };
class MapPool : public Pool { /* ... */ };
class MiscPool : public Pool { /* ... */ };

// elsewhere...

void *pnew_new(size_t size)
{
   return Pool::misc_pool()->allocate(size);
}

void *pnew_new(size_t size, Pool *pool_p)
{
   if (!pool_p) {
      return Pool::misc_pool()->allocate(size);
   }
   else {
      return pool_p->allocate(size);
   }
}

void pnew_delete(void *p)
{
   Pool *hp = Pool::find_pool(p);
   // note: if p == 0, then Pool::find_pool(p) will return 0.
   if (hp) {
      hp->deallocate(p);
   }
}

// elsewhere...

class Obj {
public:
   // misc ctors, dtors, etc.

   // just a sampling of new/del operators
   void *operator new(size_t s)             { return pnew_new(s); }
   void *operator new(size_t s, Pool *hp)   { return pnew_new(s, hp); }
   void operator delete(void *dp)           { pnew_delete(dp); }
   void operator delete(void *dp, Pool*)    { pnew_delete(dp); }

   void *operator new[](size_t s)           { return pnew_new(s); }
   void *operator new[](size_t s, Pool* hp) { return pnew_new(s, hp); }
   void operator delete[](void *dp)         { pnew_delete(dp); }
   void operator delete[](void *dp, Pool*)  { pnew_delete(dp); }
};

// elsewhere...

ClusterPool *cp = new ClusterPool(arg1, arg2, ...);

Obj *new_obj = new (cp) Obj(arg_a, arg_b, ...);

现在您可以将对象群集在单个内存存储区中,选择一个非常快但不进行解除分配的分配器,使用内存映射以及任何其他语义,通过选择池并将其作为参数传递给对象的放置新运算符来强制执行。


1
是的。我们对此相当聪明,但这与本问题无关。 - Don Wakefield
2
@jdkoftinoff,你有实际代码示例的链接吗?对我来说似乎非常有趣! - Victor
1
@MikhailVasilyev,在实际实现中,你当然会处理这个问题。这只是示例代码。 - Don Wakefield
1
我不理解misc_pool成员函数的语法。它应该是Pool* misc_pool()吗? - LRDPRDX
1
@DonWakefield,是的,当然,我只是觉得也许我不知道一些C++语法 :) - LRDPRDX
显示剩余3条评论

57

如果您想将分配与初始化分开,则此技术很有用。STL使用放置new来创建容器元素。


44

我曾在实时编程中使用过它。我们通常不希望在系统启动后执行任何动态分配(或释放)操作,因为无法保证需要多长时间。

我可以做的是预先分配一大块内存(足够大,以容纳类可能需要的任何数量的任何内容)。然后,在运行时,一旦我弄清楚如何构建这些东西,就可以使用placement new将对象构建在我想要的位置上。我知道我使用它的情况之一是帮助创建异构的 环形缓冲区

这肯定不适合菜鸟,但这就是为什么语法看起来有点复杂的原因。


嗨TED,你能分享一下你的解决方案吗?我在考虑预分配的解决方案,但是没有取得太多进展。非常感谢! - Viet
1
实际上,异构循环缓冲区代码确实是难以正确实现的棘手部分。虽然放置新代码看起来有点可怕,但与之相比,这并不是什么麻烦。 - T.E.D.

28

我已经使用它来通过alloca()在堆栈上分配对象。

无耻的插播广告: 我在这里写过相关内容。


有趣的文章,但我不确定使用这个相比于 boost::array 的优势在哪里。你能详细解释一下吗? - GrahamS
boost::array 要求数组的大小必须是编译时常量。而这个没有这个限制。 - Ferruccio
2
@Ferruccio 这很酷,但我注意到你的宏略微不安全,即size可能是一个表达式。例如,如果传入x+1,则会将其扩展为sizeof(type)* x + 1,这是不正确的。你需要加括号使宏更安全。 - Benj
如果发生异常,使用alloca看起来很危险,因为您必须调用所有对象的析构函数。 - CashCow

19

实际上,实现任何分配比插入的元素所需的最小内存更多的数据结构都是必要的(即除了一次分配一个节点的链接结构之外的任何结构)。

以像unordered_mapvectordeque这样的容器为例。这些容器分配的内存比你迄今为止插入的元素所需的内存更多,以避免每个单独插入都需要进行堆分配。让我们以vector作为最简单的例子。

当你执行以下操作时:

vector<Foo> vec;

// Allocate memory for a thousand Foos:
vec.reserve(1000);

...这并不实际构造一千个Foos。它只是为它们分配/保留内存。如果vector在这里没有使用放置new,那么它将默认构造Foos,并且即使您从未插入过元素,也必须调用它们的析构函数。

分配!=构造,释放!=销毁

一般来说,要实现上述许多数据结构,您不能将分配内存和构造元素视为一个不可分割的事物,同样,您也不能将释放内存和销毁元素视为一个不可分割的事物。

必须在这些想法之间进行区分,以避免不必要地左右超额调用构造函数和析构函数,这就是为什么标准库将std::allocator的概念(在分配/释放内存*时不构造或销毁元素)与使用它的容器分开,后者使用放置new手动构造元素,并使用显式调用析构函数手动销毁元素。

  • 我讨厌std::allocator的设计,但这是一个不同的主题,我会避免抱怨。 :-D

总之,我倾向于经常使用它,因为我编写了一些通用的、符合标准的C++容器,这些容器无法建立在现有容器的基础上。其中包括我几十年前构建的一个小型向量实现,以避免在普通情况下进行堆分配,以及一个内存效率高的字典树(不会一次性分配一个节点)。在这两种情况下,我无法使用现有的容器来实现它们,因此我必须使用放置new来避免在不必要地左右超出事物时多余地调用构造函数和析构函数。

如果您曾经使用自定义分配器单独分配对象,例如空闲列表,那么通常也需要使用放置new,如下所示(基本示例,不考虑异常安全或RAII):

Foo* foo = new(free_list.allocate()) Foo(...);
...
foo->~Foo();
free_list.free(foo);

14

我用它创建了一个Variant类(即一个可以表示由多种不同类型中的一个构成的单个值的对象)。

如果Variant类所支持的所有值类型都是POD类型(例如int、float、double、bool),那么标记C式联合就足够了,但是如果你希望一些值类型是C++对象(例如std::string),则C联合特性无法实现,因为非POD数据类型不能作为联合的一部分声明。

所以,我分配了一个足够大的字节数组(例如sizeof(the_largest_data_type_I_support)),并在Variant设置为保存该类型的值时,在该区域使用放置new来初始化适当的C++对象。(当转换到不同的数据类型时,当然我先手动调用对象的析构函数)


3
额,非POD数据类型可以在联合中声明,只要您提供一个联合构造函数 - 嘿 - 那个构造函数_可能使用放置 new_来初始化其非POD子类。参考:https://dev59.com/F3RC5IYBdhLWcg3wXPwC#33289972 使用任意大的字节数组重新发明这个轮子是一项令人印象深刻的杂技表演,但似乎完全没有必要,那我错过了什么呢? :) - underscore_d
9
你错过了C++11之前的所有版本,而这些版本在许多情况下仍需要得到支持。 :) - Jeremy Friesner

14

首席极客:BINGO!你完全理解了-这正是它的完美用途。在许多嵌入式环境中,外部限制和/或整体使用场景迫使程序员将对象的分配与其初始化分开。C ++将它们一起称为“实例化”; 但每当必须显式调用构造函数的操作而没有动态或自动分配时,放置new就是做到这一点的方法。对于固定在硬件组件地址(内存映射I / O)上的全局C ++对象或任何由于某种原因必须驻留在固定地址的静态对象来说,这也是完美的方法。


10

在序列化(例如使用boost::serialization)时,放置new非常有用。在我十年的c++编程生涯中,这只是第二次我需要使用放置new(如果您包括面试,则为第三次:)。


10
我认为没有任何答案突出强调这一点,但是新放置方式的另一个很好的示例和用途是通过使用内存池来减少内存碎片化。这在嵌入式和高可用性系统中特别有用。在后一种情况下,这非常重要,因为对于必须连续运行24/365天的系统,没有碎片非常重要。这个问题与内存泄漏无关。
即使使用了非常好的malloc实现(或类似的内存管理函数),长时间处理内存碎片化也是非常困难的。在某个时候,如果你不巧妙地处理内存预留/释放调用,你可能会遇到很多难以重新使用(分配给新的预留)的小间隙。因此,在这种情况下使用的解决方案之一是使用内存池,预先为应用程序对象分配内存。之后,每当您需要为某个对象分配内存时,只需使用新放置方式在已经预留的内存上创建一个新对象。
这样,一旦您的应用程序启动,您已经预留了所有所需的内存。所有新的内存预留/释放都将进入分配的池中(您可以有多个池,每个池对应不同的对象类)。在这种情况下,不会发生内存碎片化,因为没有间隙,您的系统可以长时间运行(数年)而不会受到碎片化的影响。
我在实践中特别看到了这一点,尤其是针对VxWorks RTOS,因为其默认的内存分配系统受到碎片的很大影响。因此,在该项目中基本上禁止使用标准的new/malloc方法来分配内存。所有内存预留都应该进入专用的内存池。

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