我了解为特定类重载新运算符的基本知识。然而,有一件事我不明白,如果可能的话。假设我有这样一门课:
class X{
int a;
long b;
float c;
}
我希望在我的程序一开始就预先创建100个X对象。我想只调用一次新运算符,以分配(至少)(4+4+4?)x 100=1200字节。然后,无论何时调用X::new()
,而不是调用new()
(or malloc()
),我都会返回X
对象的空"外壳",然后a
、b
和c
被简单地分配给数据成员。
我该怎么做?我的问题的重点是,当我为100X对象保留1200字节时,只从内核获取一次内存。在我的程序开始后,我希望在检索X对象"shell"时执行最低限度的操作?
我问题的重点是,记忆只取自内核一次,当我为100X对象保留1200字节时。在在我的程序开始时,我希望在检索X对象"shell"?
听起来你想做的事情类似于建立一个内存池。
在内存池中,您预先分配了一个大的原始缓冲区,对象最终将驻留在该缓冲区中。稍后将在此缓冲区中分配各个对象。这样做的好处是不必为每个单独的对象分配内存——所要做的就是在预先分配的空间内构造对象。由于您不必为以这种方式实例化的每个Object
深入到内核,因此可能会节省大量时间。缺点是您负责以更直接的方式管理这些对象。代码可能很棘手、复杂并且容易出现错误。
为了分配原始缓冲区,我们只需分配一个足够大的char
数组,以容纳所有预期的Object
s:
char* buf = new char [1200];
为了完成第二部分——在内存池中构造一个对象——您需要使用placementnew。假设buf
是预分配缓冲区中您希望构建新对象p
的位置:
Object* p = new (buf) Object();
当需要销毁此对象时,请不要使用delete
。delete
将尝试解除分配对象的内存,导致未定义的行为和可能的崩溃,因为您没有为对象1分配内存。相反,这是一种必须直接调用析构函数的情况:
p->~Object();
一旦所有对象都被销毁,就可以使用delete[]
:释放缓冲区
delete [] buf;
这里有一个完整的例子,展示了如何使用placement-new
,包括缓冲区的构造。这使用(隐式定义的)默认构造函数。稍后我将展示如何使用另一个构造函数:
#include <cstdlib>
#include <new> // required for placement-new
#include <iostream>
class X
{
public:
int a;
long b;
float c;
};
int main()
{
// construct the memory pool's buffer
char* buf = new char [sizeof(X) * 1000]; // enough memory for 1000 instances of X
// Instantiate 1000 instances of X using placement-new
for (size_t i = 0; i < 1000; ++i)
{
// Where in the memory pool shoudl we put this?
char* buf_loc = buf + (sizeof(X) * i);
// Construct a new X at buf_loc
X* new_x = new (buf_loc) X;
}
// Do something with those instances
for (size_t i = 0; i < 1000; ++i)
{
// Where is the object?
char* buf_loc = buf + (sizeof(X) * i);
X* my_x = reinterpret_cast <X*> (buf_loc); // this cast is safe because I *know* that buf_loc points to an X
// Let's assign some values and dump them to screen
my_x->a = i;
my_x->b = 420000 + i;
my_x->c = static_cast <float> (i) + 0.42;
std::cout << "[" << i << "]t" << my_x->a << "t" << my_x->b << "t" << my_x->c << "n";
}
// Destroy the X's
for (size_t i = 0; i < 1000; ++i)
{
// Where is the object?
char* buf_loc = buf + (sizeof(X) * i);
X* my_x = reinterpret_cast <X*> (buf_loc); // this cast is safe because I *know* that buf_loc points to an X
// Destroy it
my_x->~X();
}
// Delete the memory pool
delete [] buf;
}
现在,让我们为X
定义一个构造函数,它接受参数:
class X
{
public:
X (int aa, long bb, float cc)
:
a (aa),
b (bb),
c (cc)
{
}
int a;
long b;
float c;
};
由于我们在这里定义了构造函数,编译器将不再为X
隐式定义默认构造函数。
让我们使用这个新的构造函数。现在必须将参数传递给构造函数:
// Instantiate 1000 instances of X using placement-new
for (size_t i = 0; i < 1000; ++i)
{
// Where in the memory pool shoudl we put this?
char* buf_loc = buf + (sizeof(X) * i);
// Construct a new X at buf_loc
X* new_x = new (buf_loc) X(0,0,0.0f);
}
不要使用delete
1:从技术上讲,在这里使用delete
会产生未定义的行为的原因是,delete
只能使用从对new
的调用返回的指针进行调用。由于您没有new
对象,但placement-new
处理了对象,因此无法调用delete
。
这几乎总是一个坏主意(有关原因的详细讨论,请参阅我的论文"重新考虑自定义内存分配";简而言之,通用分配器已经针对自定义分配器试图优化的情况进行了优化)。
无论何时考虑使用自定义分配方案,您都应该问自己以下问题:您是否实际衡量了使用默认分配器对性能的影响(它并不总是那么多,也不值得冒错误的风险,等等)?如果这真的是瓶颈,你有没有尝试过像Hoard、tcmalloc或jemalloc这样的替代分配器?
您要做的基本上是实现自己的内存管理器。您可能会发现,实现一个工厂类更容易,您可以将该类初始化为某个大型内存集,并在请求时返回实例。为了防止这些对象被其他人创建,您可以将构造函数标记为private(并使您的工厂类成为朋友)。