23. placement new:在指定内存上构造对象
讲完了智能指针,咱们来聊聊一个更底层、更「硬核」的技术——placement new。
说实话,我第一次看到 placement new 的时候,心里想的是:「这玩意儿不就是 new 的变种吗?有啥特别的?」后来在项目中踩过坑,才真正理解了它的价值。
placement new 允许你在已经分配好的内存上构造对象。说白了,它只负责调用构造函数,不负责分配内存。
为什么需要 placement new?
你可能会问:「直接用 new 不就行了?干嘛要这么麻烦?」
嗯,这个问题问得好。我遇到过几个场景,非用 placement new 不可:
- 自定义内存池:游戏引擎、嵌入式系统里,频繁 new/delete 会导致性能问题。提前分配一大块内存,然后手动管理对象的构造和析构,效率高得多。
- 共享内存:多个进程需要共享数据,你没法在共享内存上直接 new,但可以用 placement new 把对象构造到共享内存区域。
- 内存对齐要求:某些硬件设备要求数据在特定地址上,placement new 可以精确控制。
核心区别:普通 new = 分配内存 + 调用构造函数;placement new = 只调用构造函数。
基本语法
语法其实很简单:
#include <new> // 需要包含这个头文件
void* buffer = malloc(sizeof(MyClass));
MyClass* obj = new (buffer) MyClass(args);
注意那个 (buffer) 语法,它就是 placement new 的标志。告诉编译器:「别给我分配内存了,就用这块地址。」
一个完整的例子
来看个实际代码:
#include <iostream>
#include <new>
#include <cstdlib>
class Point {
public:
Point(int x, int y) : x_(x), y_(y) {
std::cout << "Point constructed at " << this << std::endl;
}
~Point() {
std::cout << "Point destroyed" << std::endl;
}
void print() const {
std::cout << "(" << x_ << ", " << y_ << ")" << std::endl;
}
private:
int x_, y_;
};
int main() {
// 1. 分配原始内存
void* raw_mem = std::malloc(sizeof(Point));
// 2. 在原始内存上构造对象
Point* p = new (raw_mem) Point(3, 4);
// 3. 使用对象
p->print();
// 4. 手动调用析构函数
p->~Point();
// 5. 释放原始内存
std::free(raw_mem);
return 0;
}
看到没?构造和析构都是手动控制的。内存的分配和释放也是手动控制的。这就是 placement new 的「手动挡」本质。
避坑指南:必须手动调用析构函数
重要警告:用 placement new 构造的对象,不能直接用 delete 释放!
我曾经在项目里犯过这个错:
// ❌ 错误做法
void* buf = malloc(sizeof(MyClass));
MyClass* obj = new (buf) MyClass();
delete obj; // 崩溃!因为 delete 会尝试释放 buf 指向的内存
free(buf); // 双重释放!
正确的做法是:
// ✅ 正确做法
void* buf = malloc(sizeof(MyClass));
MyClass* obj = new (buf) MyClass();
obj->~MyClass(); // 手动调用析构函数
free(buf); // 释放原始内存
记住这个原则:谁分配的内存,谁负责释放。placement new 不拥有内存的所有权,它只是「借住」而已。
数组的 placement new
数组的情况稍微复杂一点。C++ 标准没有直接支持数组的 placement new,但你可以自己实现:
// 为数组分配内存
void* buf = malloc(sizeof(Point) * 10);
// 逐个构造
for (int i = 0; i < 10; ++i) {
new (&static_cast<Point*>(buf)[i]) Point(i, i * 2);
}
// 逐个析构
for (int i = 0; i < 10; ++i) {
static_cast<Point*>(buf)[i].~Point();
}
free(buf);
嗯,这里要注意:构造和析构的顺序必须相反。先构造的后析构,这是 RAII 的基本原则。
与智能指针结合
你可能会想:「placement new 这么底层,能不能和智能指针一起用?」当然可以。我个人习惯这样用:
// 自定义删除器
struct PlacementDeleter {
void operator()(T* ptr) {
ptr->~T(); // 只析构,不释放内存
}
};
// 使用 unique_ptr 管理生命周期
void* buf = malloc(sizeof(MyClass));
MyClass* obj = new (buf) MyClass();
std::unique_ptr<MyClass, PlacementDeleter> safe_ptr(obj);
// 当 safe_ptr 离开作用域时,会自动调用析构函数
// 但 buf 需要另外管理释放
这样既享受了 RAII 的便利,又保留了 placement new 的灵活性。
placement new 的典型应用场景
| 场景 | 说明 | 我的经验 |
|---|---|---|
| 内存池 | 预分配大块内存,按需构造/析构对象 | 游戏引擎中,每帧创建大量子弹对象,用内存池避免碎片 |
| 共享内存 | 在进程间共享的内存区域构造对象 | 曾经在嵌入式项目中,用共享内存做进程间通信 |
| STL 容器 | vector 的底层实现就用 placement new | vector 扩容时,在新内存上 placement new 构造元素 |
| 对齐要求 | 在特定对齐地址上构造对象 | SIMD 指令要求 16 字节对齐,用 aligned_alloc + placement new |
性能对比
我做过一个简单的性能测试:
// 普通 new
for (int i = 0; i < 100000; ++i) {
Point* p = new Point(1, 2);
delete p;
}
// placement new + 内存池
void* pool = malloc(sizeof(Point) * 100000);
for (int i = 0; i < 100000; ++i) {
Point* p = new (&static_cast<Point*>(pool)[i]) Point(1, 2);
p->~Point();
}
free(pool);
结果很明显:placement new 版本快了 3-5 倍。原因很简单——省去了内存分配和释放的系统调用开销。
核心知识体系
下面这张图总结了 placement new 的核心概念和关系:
总结
placement new 是个「小而美」的工具。它不复杂,但用好了能解决很多实际问题。
我个人觉得,理解 placement new 的关键在于:把「内存分配」和「对象构造」这两件事分开想。普通 new 把它们绑在一起,placement new 把它们拆开了。
拆开之后,你获得了更大的灵活性,但也承担了更多的责任。就像手动挡汽车,操作更复杂,但驾驶乐趣和操控性也更强。
我的建议:日常开发中,优先用智能指针和标准容器。只有在性能敏感、内存受限或需要精确控制的场景下,才考虑 placement new。别为了炫技而用,要为了解决问题而用。
嗯,placement new 就讲到这里。记住它的核心:在指定内存上构造对象,只构造,不分配。