Placement new 是 C++ 中的一个特殊功能,允许开发者在已经分配的内存位置上直接构造对象。这种技术特别适用于内存管理敏感的应用,如嵌入式系统、游戏开发、实时系统等,其中控制内存分配和避免额外的内存分配开销非常关键。
例如在日志模块中,通常需要使用单例模式来实现一个全局唯一的对象用来写入日志。而通过 Placement new 作为单例模式申请内存确保了被使用的内存不被释放掉,最终实现了生命周期和程序的整个生命周期等同。
new 的两个操作
当我们在 C++ 中使用普通的 new
操作符时,它实际上执行了两个操作:
-
分配内存:
new
操作符首先在堆上分配足够的内存来存储指定类型的对象。这是通过调用内存分配函数operator new
来完成的,该函数接受一个参数,即要分配的字节数。 -
在分配的内存中构造对象:一旦内存被分配,
new
操作符会在这块内存上构造对象。这是通过调用对象的构造函数来完成的。
以下是一个简单的示例,说明了这两个步骤:
class MyClass {
public:
MyClass() {
// Constructor code
}
};
int main() {
MyClass* myObject = new MyClass(); // 使用 new 操作符分配内存并构造对象
// ...
delete myObject; // 使用 delete 操作符释放内存并析构对象
return 0;
}
在上述代码中,new MyClass()
首先调用 operator new
函数来在堆上分配足够的内存来存储一个 MyClass
对象。然后,它在这块内存上调用 MyClass
的构造函数来构造一个新的 MyClass
对象。最后,它返回一个指向新构造的对象的指针,我们可以使用这个指针来访问对象。
当我们不再需要这个对象时,我们可以使用 delete
操作符来释放对象占用的内存并调用其析构函数。这是通过首先调用对象的析构函数,然后调用内存释放函数 operator delete
来完成的。
Placement new
在 C++ 中,placement new 是一种特殊的 new 操作符,它允许我们在已经分配的内存上构造对象。这样,我们就可以更加灵活地控制对象的内存管理,例如在特定位置创建对象,或者在已经分配的内存中重新构造对象。
以下是一个简单的示例,说明了如何使用 placement new:
#include <new> // 必须包含这个头文件来使用 placement new
class MyClass {
public:
MyClass() {
// Constructor code
}
};
int main() {
char buffer[sizeof(MyClass)]; // 分配足够的内存来存储 MyClass 对象
MyClass* myObject = new (buffer) MyClass(); // 在 buffer 上构造 MyClass 对象
// 使用 myObject...
myObject->~MyClass(); // 显式调用析构函数来析构对象,因为我们不能使用 delete
return 0;
}
在上述代码中,我们首先在栈上分配了一个足够大的缓冲区来存储一个 MyClass
对象。然后,我们使用 placement new (new (buffer) MyClass()
) 在这个缓冲区上构造了一个 MyClass
对象。注意,我们需要传递一个指向已分配内存的指针给 placement new。
当我们不再需要这个对象时,我们不能使用 delete
操作符来释放内存和析构对象,因为内存并不是由 new
操作符分配的。相反,我们需要显式地调用对象的析构函数 (myObject->~MyClass()
) 来析构对象。然而,我们不需要手动释放内存,因为它是在栈上分配的,当它离开作用域时,编译器会自动释放它。
区别
综上普通的 new 说在堆上申请空间并构造对象,而 placement new 可以自己确定在堆上还是在栈上申请空间。
在 C++ 中,当我们使用普通的 new
操作符时,它会在堆上分配内存,并返回一个指向新分配内存的指针。我们通常不知道这个指针的具体值,也就是说,我们不知道对象实际存储在内存的哪个位置。
class MyClass {
public:
MyClass() {
// Constructor code
}
};
int main() {
MyClass* myObject = new MyClass(); // 使用 new 操作符分配内存并构造对象
// 我们不知道 myObject 指向的具体内存地址
// ...
delete myObject; // 使用 delete 操作符释放内存并析构对象
return 0;
}
然而,当我们使用 placement new 时,我们可以指定对象应该被构造在哪个位置。这是通过传递一个指向已分配内存的指针给 placement new 来实现的。
#include <new> // 必须包含这个头文件来使用 placement new
class MyClass {
public:
MyClass() {
// Constructor code
}
};
int main() {
char buffer[sizeof(MyClass)]; // 分配足够的内存来存储 MyClass 对象
MyClass* myObject = new (buffer) MyClass(); // 在 buffer 上构造 MyClass 对象
// 我们知道 myObject 指向的具体内存地址,就是 buffer 的地址
// ...
myObject->~MyClass(); // 显式调用析构函数来析构对象,因为我们不能使用 delete
return 0;
}
在上述代码中,我们知道 myObject
指向的具体内存地址,因为它就是我们分配的 buffer
的地址。
当我们使用普通的 new
操作符分配内存时,我们可以使用 delete
操作符来释放内存并调用对象的析构函数。然而,对于使用 placement new 的情况,我们不能使用 delete
来释放内存,因为内存并不是由 new
操作符分配的。相反,我们需要显式地调用对象的析构函数来析构对象。然而,我们不需要手动释放内存,因为它是在栈上分配的,当它离开作用域时,编译器会自动释放它。
内存释放
在 C++ 中,placement new 的语法是 new (address) Type(initializer)
,其中 address
是一个指向已分配内存的指针,Type
是要构造的对象的类型,initializer
是传递给 Type
构造函数的参数。
以下是一个简单的示例,说明了如何使用 placement new:
#include <new> // 必须包含这个头文件来使用 placement new
class MyClass {
public:
int value;
MyClass(int v) : value(v) {
// Constructor code
}
};
int main() {
char buffer[sizeof(MyClass)]; // 分配足够的内存来存储 MyClass 对象
MyClass* myObject = new (buffer) MyClass(42); // 在 buffer 上构造 MyClass 对象,传递 42 给构造函数
// 使用 myObject...
myObject->~MyClass(); // 显式调用析构函数来析构对象,因为我们不能使用 delete
return 0;
}
在上述代码中,我们首先在栈上分配了一个足够大的缓冲区来存储一个 MyClass
对象。然后,我们使用 placement new (new (buffer) MyClass(42)
) 在这个缓冲区上构造了一个 MyClass
对象,并传递了 42
给 MyClass
的构造函数。
当我们不再需要这个对象时,我们不能使用 delete
操作符来释放内存和析构对象,因为内存并不是由 new
操作符分配的。相反,我们需要显式地调用对象的析构函数 (myObject->~MyClass()
) 来析构对象。然而,此处是不需要手动释放内存,因为它是在栈上分配的,当它离开作用域时,编译器会自动释放它。
placement new 使用场景
在 C++ 中,placement new 的使用场景通常是在需要对内存管理进行精细控制的情况下。以下是一些可能会优先使用 placement new 的情况:
- 内存池:如果你正在实现一个内存池,那么你可能需要在特定的内存位置上构造对象。在这种情况下,你可以使用 placement new 在内存池中的特定位置上构造对象。
#include <new>
class MyClass {
public:
MyClass() {
// Constructor code
}
};
char memoryPool[1000]; // 内存池
int main() {
MyClass* myObject = new (memoryPool) MyClass(); // 在内存池上构造对象
// 使用 myObject...
myObject->~MyClass(); // 显式调用析构函数来析构对象
return 0;
}
- 对象重用:如果你有一个对象,你想在相同的内存位置上构造一个新的对象,那么你可以使用 placement new。这可以避免内存分配和释放的开销,提高性能。
#include <new>
class MyClass {
public:
MyClass() {
// Constructor code
}
};
int main() {
MyClass* myObject = new MyClass(); // 使用 new 构造对象
// 使用 myObject...
myObject->~MyClass(); // 显式调用析构函数来析构对象
myObject = new (myObject) MyClass(); // 在相同的内存位置上构造新的对象
// 使用新的 myObject...
delete myObject; // 使用 delete 来释放内存并析构对象
return 0;
}
在上述两个例子中,我们都使用了 placement new 来在特定的内存位置上构造对象。这可以提供更大的灵活性,允许我们更精细地控制内存管理。
placement new 的优点
在 C++ 中,placement new 操作符相比于普通的 new 操作符有以下优点:
-
预知内存地址:使用 placement new,我们可以在已经分配的内存上构造对象。这意味着我们可以预先知道对象的内存地址。
-
内存管理优化:在构建内存池、垃圾收集器或者在性能和异常安全性至关重要的情况下,placement new 非常有用。因为它允许我们在特定的内存位置上构造对象,这可以避免内存分配和释放的开销,提高性能。
-
减少分配失败的风险:由于内存已经被分配,所以使用 placement new 没有分配失败的风险。此外,由于不需要进行内存分配,所以在预分配的缓冲区上构造对象的时间通常会更少。
-
资源有限的环境:在资源有限的环境中,例如嵌入式系统,预先分配内存并使用 placement new 可以更好地控制内存使用,避免动态内存分配可能带来的问题。