placement new 是 C++ 提供的一种特殊重载形式的 operator new,它允许开发者在已经分配好的原始内存上构造对象。换句话说,普通 new 会先申请内存再调用构造函数,而 placement new 跳过内存申请这一步,只负责在指定地址上调用构造函数。这一点在实现内存池、共享内存对象、嵌入式系统以及实时应用中非常有用,因为在这些场景下内存通常已经由开发者准备好,只需要把对象精确地放置进去。接下来我们结合代码仔细拆解它的用法。

一、placement new 的基本语法和与普通 new 的区别
使用 placement new 之前需要包含头文件 <new>。它的语法形式是 new (address) Type(args),其中 address 是一个指向原始内存的指针,Type 是要构造的类型,args 是传给构造函数的参数。执行完这条语句后,Type 的构造函数会在 address 指向的内存上运行,整个表达式返回一个 Type* 指针,通常这个指针的值就等于 address 转换后的地址。例如下面的代码在栈上分配了一块字符缓冲区,并在它上面构造了一个 Widget 对象:
#include <new>
#include <iostream>
class Widget {
public:
Widget(int v) : value_(v) {
std::cout << "构造 Widget,值 = " << value_ << '\n';
}
~Widget() {
std::cout << "析构 Widget,值 = " << value_ << '\n';
}
void show() const {
std::cout << "当前值 = " << value_ << '\n';
}
private:
int value_;
};
int main() {
alignas(Widget) char buffer[sizeof(Widget)];
Widget* w = new (buffer) Widget(42);
w->show();
w->~Widget();
return 0;
}
这里 buffer 是一块已经存在的内存,placement new 在它上面执行了 Widget 的构造函数。可以看到,整个过程中没有调用系统级的内存分配函数,对象直接出现在我们预先准备好的地址上。这对于需要精确控制内存位置的应用来说非常关键。和普通 new 相比,placement new 不分配内存、不释放内存,也不跟踪对象的生命周期,因此开发者必须自己保证内存足够大、对齐正确,并且在使用完毕后手动析构。
如果构造函数带多个参数,可以直接把参数写在类型后面,就像普通对象构造一样。例如 new (buffer) Widget(10, "test") 这种形式完全合法。需要注意的是,placement new 不会检查目标内存是否已经存储了其他对象。如果同一块内存上已经构造过一个对象,而又没有先调用析构函数,那么直接再 placement new 一次会造成旧对象的资源泄漏,甚至产生未定义行为。
二、手动调用析构函数与生命周期管理
由于 placement new 只负责构造,C++ 并没有一个对应的 placement delete 来自动调用析构函数,因此对象的生命周期必须由开发者手动终结。做法是显式调用析构函数,语法为 w->~Widget();。这一调用会执行 Widget 的析构函数体,释放对象内部持有的资源,但不会释放对象占用的底层内存。底层内存仍然存在,可以被重新用于构造其他对象,也可以在之后由开发者统一释放。
还有一点必须注意:不能对 placement new 构造的对象使用 delete w;。因为 delete 会先调用析构函数,然后把指针交给对应的 operator delete 去释放内存。而这块内存并不是通过普通 new 从堆上申请的,它可能位于栈上、内存池中或者共享内存中,直接 delete 会导致未定义行为,通常表现为程序崩溃或堆损坏。下面是一个使用动态内存作为底层存储的完整示例:
#include <cstdlib>
#include <new>
#include <iostream>
class Resource {
public:
Resource(int id) : id_(id) {
std::cout << "Resource " << id_ << " 构造\n";
}
~Resource() {
std::cout << "Resource " << id_ << " 析构\n";
}
void run() const {
std::cout << "Resource " << id_ << " 运行中\n";
}
private:
int id_;
};
int main() {
void* mem = std::aligned_alloc(alignof(Resource), sizeof(Resource));
if (!mem) return 1;
Resource* r = new (mem) Resource(7);
r->run();
r->~Resource(); // 显式析构对象
std::free(mem); // 再释放底层内存
return 0;
}
释放顺序非常重要:一定是先调用析构函数,再释放底层内存。如果先释放内存,对象还认为自己存活在那一块地址上,后续再析构就会访问已经归还给系统的内存,造成严重的未定义行为。如果对象内部有堆内存、文件句柄、锁或线程资源,析构函数会负责正确清理这些资源,因此遗忘析构就等于泄漏了这些资源。
重复析构也是需要避免的。对同一个地址调用两次析构函数,逻辑上等于销毁一个已经销毁过的对象,这是未定义行为。在实际工程中,通常会封装一个 RAII 辅助类,在作用域结束时自动调用析构并归还内存,减少手动管理出错的可能。
三、内存对齐与缓冲区准备
placement new 对目标地址的对齐要求与普通 new 完全一致。也就是说,传给 placement new 的地址必须满足 alignof(Type) 的对齐要求。比如某个结构体的对齐值是 8,那么存储它的地址必须能被 8 整除。如果缓冲区没有正确对齐,程序可能不会在当下立即崩溃,但后续访问成员变量时可能触发硬件异常,或者出现数据错乱。不同平台对未对齐访问的容忍度不一样,x86 通常会慢一些,而 ARM、MIPS 等平台往往直接抛异常。
最稳妥的栈上缓冲区写法是使用 alignas 指定对齐方式。下面是一个包含 double 成员的结构体示例:
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <new>
struct Packed {
char c;
double d;
};
int main() {
std::cout << "sizeof(Packed) = " << sizeof(Packed) << '\n';
std::cout << "alignof(Packed) = " << alignof(Packed) << '\n';
alignas(Packed) unsigned char storage[sizeof(Packed)];
bool aligned = (reinterpret_cast<std::uintptr_t>(storage) % alignof(Packed) == 0);
std::cout << "storage 是否对齐: " << aligned << '\n';
Packed* p = new (storage) Packed{'A', 3.14};
p->~Packed();
return 0;
}
如果底层内存是动态分配的,使用 malloc 时要特别注意。C 标准规定 malloc 返回的内存对齐到 max_align_t,但某些类型的对齐要求可能超过这个默认值。此时应该使用 C++17 提供的带对齐参数的 operator new,或者 C11 的 aligned_alloc。例如前面示例中使用的 std::aligned_alloc(alignof(Resource), sizeof(Resource)) 就能保证返回的地址满足 Resource 的对齐要求。
当需要构造对象数组时,除了对齐,还必须正确计算总内存大小。不要简单使用 new char[n * sizeof(T)] 然后直接 placement new 数组,因为 char 数组默认对齐只有 1,很可能不满足 T 的对齐要求。更安全的做法是直接分配 alignas(T) 的缓冲区,或者使用标准库提供的 std::allocator<T> 来获取对齐内存。
四、在内存池中使用 placement new 构造对象
内存池的核心思想是一次性从系统申请一大块内存,后续对象构造都在这块内存上进行,从而避免频繁调用 malloc/new 带来的碎片和锁开销。placement new 正好承担构造职责。下面给出一个简化版的定长内存池实现,它从预分配的内存中取出空闲块,并用 placement new 在块上构造对象:
#include <new>
#include <vector>
#include <utility>
#include <cstddef>
#include <iostream>
template <typename T>
class FixedPool {
public:
explicit FixedPool(std::size_t count) {
element_size_ = sizeof(T);
// 使用带对齐参数的 operator new 一次性分配 count 个对象的内存
storage_ = static_cast<char*>(::operator new(count * element_size_,
std::align_val_t(alignof(T))));
free_blocks_.reserve(count);
for (std::size_t i = 0; i < count; ++i) {
free_blocks_.push_back(storage_ + i * element_size_);
}
}
template <typename... Args>
T* create(Args&&... args) {
if (free_blocks_.empty()) return nullptr;
void* addr = free_blocks_.back();
free_blocks_.pop_back();
// 在已回收的地址上构造对象
return new (addr) T(std::forward<Args>(args)...);
}
void destroy(T* p) {
if (!p) return;
p->~T();
free_blocks_.push_back(reinterpret_cast<char*>(p));
}
~FixedPool() {
// 注意:析构时不能假设所有对象都已经归还
::operator delete(storage_, std::align_val_t(alignof(T)));
}
private:
char* storage_ = nullptr;
std::size_t element_size_ = 0;
std::vector<char*> free_blocks_;
};
struct Task {
int id;
explicit Task(int i) : id(i) {
std::cout << "Task " << id << " 构造\n";
}
~Task() {
std::cout << "Task " << id << " 析构\n";
}
};
int main() {
FixedPool<Task> pool(4);
Task* t1 = pool.create(1);
Task* t2 = pool.create(2);
pool.destroy(t1);
pool.destroy(t2);
return 0;
}
在这个池子中,create 负责从空闲列表取出一个地址,并在该地址上调用 T 的构造函数。由于内存已经存在,整个过程没有系统分配调用。销毁对象时 destroy 先调用析构函数,再把地址放回空闲列表,这样这块内存就可以被下一个对象复用。这种模式在游戏开发、高频交易系统和嵌入式实时环境里非常常见。
需要强调,内存池只管理内存,不管理对象生命周期,生命周期由使用方负责。如果希望更安全,可以用智能指针配合自定义删除器。例如把 Task* 放进 std::unique_ptr,删除器只调用 pool.destroy 而不释放内存,这样即使异常抛出,对象也能被正确析构并归还到池中。这种组合兼顾了性能和安全性。
五、常见错误与异常安全注意事项
使用 placement new 最容易犯的错误可以归纳为几类。第一是忘记调用析构函数,导致对象内部资源泄漏。第二是对栈上或内存池中的对象直接调用 delete,这会破坏内存管理结构。第三是重复析构同一个地址,等于对一个已经销毁的对象再次执行析构逻辑。第四是没有检查内存对齐,尤其在动态分配场景下默认 malloc 可能不满足超对齐类型的要求。
异常安全也是一个容易忽略的点。假设构造函数在 placement new 执行过程中抛出异常,那么 C++ 运行时会负责析构已经构造成功的成员变量和基类子对象,但不会调用完整对象的析构函数。同时,因为内存本来就是外部提供的,placement new 不会释放这块内存。所以如果构造失败,我们不应该在 catch 块中对目标地址调用析构函数,因为对象根本没有构造完成。下面是一个错误示范:
#include <new>
#include <cstdlib>
#include <iostream>
struct MayThrow {
int value = 0;
MayThrow() = default;
MayThrow(int v) {
if (v < 0) throw std::runtime_error("negative");
value = v;
}
~MayThrow() {
std::cout << "MayThrow 析构\n";
}
};
int main() {
void* mem = std::aligned_alloc(alignof(MayThrow), sizeof(MayThrow));
try {
MayThrow* p = new (mem) MayThrow(-1);
p->~MayThrow();
} catch (...) {
// 构造函数失败,对象并没有完整构造,不要在这里调用 p->~MayThrow()
std::cout << "构造函数抛出了异常\n";
}
std::free(mem);
return 0;
}
数组形式的 placement new 也有额外陷阱。new (addr) T[n] 通常会在 addr 前面记录数组元素个数,因此它实际需要的内存可能大于 n * sizeof(T),而具体大小因编译器而异。销毁时应该对每个元素逐个调用析构函数,而不是直接对数组指针调用 delete[]。实际工程中,为了避免这些编译器相关的细节,更推荐使用循环逐个构造单个对象,而不是使用数组形式的 placement new。
总体来说,placement new 是一把非常锋利的工具。它把内存分配和对象构造这两件事彻底分开,让开发者可以完全掌控对象的物理位置和生命周期。但权力越大,责任也越大。只要严格遵循“先析构、后释放”“确保对齐”“谨慎处理异常”这几条原则,就能在内存池、共享内存、缓冲区复用等场景中发挥出普通 new 无法替代的作用。
C++ placement new指定内存地址对象构造修改时间:2026-10-04 01:28:43