在 C++ 内存管理中,new 运算符有两种重要形式:普通 new 和 placement new。理解它们的区别对于编写高效、内存安全的代码至关重要。
目录(Contents)
📋 快速对比表
| 特性 | Placement New | 普通 New |
|---|---|---|
| 内存来源 | 调用者提供 | 堆分配 |
| 语法 | new (地址) 类型(参数) | new 类型(参数) |
| 内存管理 | 调用者管理 | new/delete 管理 |
| 性能 | 无分配开销 | 有分配开销 |
| 使用场景 | 内存池、容器、性能优化 | 常规动态分配 |
🚀 基本语法对比
普通 New(堆分配)
cpp
// 分配内存并在堆上构造对象 MyClass* obj = new MyClass(arg1, arg2); // 使用后必须 delete delete obj;
Placement New(指定位置构造)
cpp
#include <new> // 需要包含此头文件 // 预先分配内存 char buffer[sizeof(MyClass)]; // 在指定内存上构造对象 MyClass* obj = new (buffer) MyClass(arg1, arg2); // 必须显式调用析构函数 obj->~MyClass(); // 注意:不要 delete obj! buffer 会自动释放
🔍 核心区别详解
1. 内存管理责任
普通 New:new 负责内存分配和对象构造,delete 负责析构和内存释放。
cpp
// 编译器生成类似这样的代码: void* ptr = operator new(sizeof(MyClass)); // 1. 分配内存 MyClass::MyClass(ptr, args...); // 2. 构造对象
Placement New:只负责对象构造,内存由调用者管理。
cpp
// 编译器生成类似这样的代码: void* ptr = operator new(sizeof(MyClass), (void*)address); // 不分配内存 MyClass::MyClass(ptr, args...); // 构造对象
2. 错误处理方式
cpp
// 普通 New - 可能抛出 std::bad_alloc
try {
MyClass* obj = new MyClass();
} catch (const std::bad_alloc& e) {
// 处理内存不足
}
// Placement New - 不分配内存,不会抛出 bad_alloc
// 但如果构造失败,可能抛出其他异常
💡 主要应用场景
应用1:自定义内存池
cpp
class MemoryPool {
private:
char pool[1024 * 1024]; // 1MB 内存池
size_t offset = 0;
public:
template<typename T, typename... Args>
T* allocate(Args&&... args) {
if (offset + sizeof(T) > sizeof(pool)) {
return nullptr; // 池已满
}
void* ptr = pool + offset;
T* obj = new (ptr) T(std::forward<Args>(args)...);
offset += sizeof(T);
return obj;
}
};
// 使用
MemoryPool pool;
MyClass* obj = pool.allocate<MyClass>(arg1, arg2);
obj->~MyClass(); // 必须显式析构
应用2:容器类的实现(如 std::vector、UE 的 TArray)
cpp
template<typename T>
class SimpleVector {
private:
T* data = nullptr;
size_t size = 0;
size_t capacity = 0;
public:
void push_back(const T& value) {
if (size >= capacity) {
reserve(capacity == 0 ? 4 : capacity * 2);
}
// 使用 placement new 在已分配内存上构造
new (data + size) T(value);
++size;
}
~SimpleVector() {
// 必须显式调用每个元素的析构函数
for (size_t i = 0; i < size; ++i) {
data[i].~T();
}
delete[] reinterpret_cast<char*>(data);
}
};
应用3:性能敏感场景
cpp
// 避免频繁内存分配的开销
void processObjects() {
// 一次性分配多个对象所需内存
constexpr size_t count = 1000;
char* buffer = new char[count * sizeof(MyClass)];
for (size_t i = 0; i < count; ++i) {
// 使用 placement new 构造
MyClass* obj = new (buffer + i * sizeof(MyClass)) MyClass();
// 使用对象...
obj->~MyClass(); // 显式析构
}
delete[] buffer; // 一次性释放所有内存
}
应用4:对齐内存构造
cpp
#include <cstddef>
struct alignas(64) CacheLineAligned {
int data[16];
};
void demo() {
// 确保内存对齐
alignas(CacheLineAligned) char buffer[sizeof(CacheLineAligned)];
// 在对齐的内存上构造对象
CacheLineAligned* obj = new (buffer) CacheLineAligned();
obj->~CacheLineAligned();
}
⚠️ 注意事项和最佳实践
1. 正确配对使用
cpp
// ✅ 正确做法 void* memory = malloc(sizeof(MyClass)); MyClass* obj = new (memory) MyClass(); obj->~MyClass(); // 必须调用! free(memory); // 释放原始内存 // ❌ 错误做法 void* memory = malloc(sizeof(MyClass)); MyClass* obj = new (memory) MyClass(); delete obj; // 错误!会双重释放内存
2. 异常安全
cpp
template<typename T, typename... Args>
T* construct_at(void* location, Args&&... args) {
try {
return new (location) T(std::forward<Args>(args)...);
} catch (...) {
// 构造失败时,location 保持原样
return nullptr;
}
}
3. C++17 后的新特性
cpp
#include <memory> // C++17 提供了更安全的方式 void* memory = std::aligned_alloc(alignof(MyClass), sizeof(MyClass)); // 使用 std::construct_at(内部使用 placement new) MyClass* obj = std::construct_at(reinterpret_cast<MyClass*>(memory), args...); // 使用 std::destroy_at(内部调用析构函数) std::destroy_at(obj); std::free(memory);
🎯 实际案例:Unreal Engine 的 TArray
在 UE 引擎中,TArray 使用 placement new 来实现类型安全的数组构造:
cpp
// UE 源码中的简化示例
template<typename ElementType>
class TArray {
void Add(const ElementType& Item) {
if (需要扩容) {
reserve(newCapacity);
}
// 在数组末尾使用 placement new 构造新元素
new (Data + Num) ElementType(Item);
++Num;
}
~TArray() {
// 必须显式析构所有元素
for (int32 i = 0; i < Num; ++i) {
Data[i].~ElementType();
}
FreeMemory(Data);
}
};
📝 总结要点
- Placement new 不分配内存,只在已有内存上构造对象
- 必须显式调用析构函数,但不要使用
delete - 内存生命周期由调用者管理,placement new 只负责构造
- 适用于性能敏感、自定义内存管理的场景
- C++17 提供了更安全的替代品:
std::construct_at和std::destroy_at
🔧 选择建议
- 使用 普通 new 当:需要简单的动态分配,不想管理内存细节
- 使用 placement new 当:
- 实现自定义容器或内存池
- 需要精确控制对象生命周期
- 性能至关重要,需要避免分配开销
- 需要在特定对齐的内存上构造对象
理解这两种 new 的区别,能帮助你编写更高效、更可控的 C++ 代码,特别是在游戏开发、嵌入式系统等对性能有严格要求的领域。
