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 不分配内存 只调用构造函数 手动管理生命周期 必须手动调用析构 精确控制地址 指定内存位置 典型应用场景 内存池 共享内存 STL 容器 对齐要求 ⚠ 注意事项 不能使用 delete | 必须手动析构 | 谁分配谁释放

总结

placement new 是个「小而美」的工具。它不复杂,但用好了能解决很多实际问题。

我个人觉得,理解 placement new 的关键在于:把「内存分配」和「对象构造」这两件事分开想。普通 new 把它们绑在一起,placement new 把它们拆开了。

拆开之后,你获得了更大的灵活性,但也承担了更多的责任。就像手动挡汽车,操作更复杂,但驾驶乐趣和操控性也更强。

我的建议:日常开发中,优先用智能指针和标准容器。只有在性能敏感、内存受限或需要精确控制的场景下,才考虑 placement new。别为了炫技而用,要为了解决问题而用。

嗯,placement new 就讲到这里。记住它的核心:在指定内存上构造对象,只构造,不分配