在C++的标准内存分配体系中,常规new操作符会完成两步操作:先向堆申请足够大小的内存空间,再在该空间上调用对应类型的构造函数完成对象初始化。但在一些特殊场景下,比如我们已经提前分配好了一块内存、需要操作硬件映射的固定地址内存、或者希望避免频繁堆内存申请带来的性能开销时,常规new的自动内存申请逻辑就不再适用,这时候就需要用到placement new这种特殊的new重载形式。

placement new的核心概念
placement new是C++对new操作符的一种重载实现,它的函数原型在标准库中定义为:
// 标准库中的placement new原型,位于<new>头文件中 void* operator new(std::size_t size, void* ptr) noexcept;
从原型可以看出,它接收两个参数:第一个参数是需要分配的对象大小,第二个参数是指定的已存在内存地址。和常规new不同,placement new不会额外申请任何内存,只会返回传入的ptr地址,然后在该地址上调用对应类型的构造函数完成对象构造。需要注意的是,使用placement new必须包含<new>头文件,否则编译器可能无法识别该重载形式。
placement new的适用场景
placement new主要用在以下几类特殊内存分配场景中:
- 预分配内存池场景:提前申请一大块连续内存作为内存池,后续需要构造对象时,直接从内存池中选取空闲地址使用placement new构造,避免频繁调用常规new向堆申请内存,减少内存碎片和申请开销。
- 固定地址内存操作场景:比如操作硬件映射的内存区域、共享内存段等,这些内存的地址是固定的,无法用常规new申请,只能通过placement new在指定地址上构造对象。
- 栈内存上构造动态对象场景:如果需要在栈上的一块内存空间中构造对象,也可以使用placement new,不过需要注意栈内存的生命周期管理。
placement new的使用步骤
使用placement new完成对象构造通常分为三步:
第一步:准备已分配的内存空间
首先需要有一块已经分配好的、大小足够容纳目标对象的内存空间,这块内存可以是堆上提前申请的、栈上定义的,也可以是固定的硬件地址内存。需要注意内存的对齐要求,目标对象的内存对齐要求必须和准备的内存空间对齐规则匹配,否则可能出现未定义行为。
第二步:调用placement new构造对象
使用new(ptr) Type(args)的语法调用placement new,其中ptr是准备好的内存地址,Type是要构造的对象类型,args是传递给构造函数的参数。这时候placement new会在ptr指向的地址上调用Type的构造函数,完成对象初始化。
第三步:手动调用析构函数释放对象
因为placement new没有申请新的内存,所以不能使用常规的delete来释放对象,必须手动调用对象的析构函数完成资源清理。如果这块内存后续还要复用,析构后还可以继续用placement new构造新的对象。
第四步:释放原始内存(如果需要)
如果原始内存是之前手动申请的(比如用malloc或者常规new申请的),在不再需要的时候,需要手动释放对应的原始内存,避免内存泄漏。
使用示例代码
下面通过几个不同场景的示例演示placement new的具体用法:
示例1:在预分配的堆内存上使用placement new
#include <iostream>
#include <new> // 必须包含该头文件才能使用placement new
class Demo {
public:
Demo(int val) : value(val) {
std::cout << "Demo构造函数被调用,value=" << value << std::endl;
}
~Demo() {
std::cout << "Demo析构函数被调用,value=" << value << std::endl;
}
void print() {
std::cout << "当前value值为:" << value << std::endl;
}
private:
int value;
};
int main() {
// 第一步:预分配足够大小的内存,这里用malloc分配,也可以用常规new char[]分配
void* raw_mem = malloc(sizeof(Demo));
if (raw_mem == nullptr) {
std::cerr << "内存分配失败" << std::endl;
return 1;
}
// 第二步:使用placement new在预分配的内存上构造Demo对象
Demo* obj = new(raw_mem) Demo(10);
// 调用对象方法验证构造成功
obj->print();
// 第三步:手动调用析构函数,注意不能用delete obj,否则会尝试释放raw_mem导致错误
obj->~Demo();
// 第四步:如果需要复用内存,可以再次用placement new构造新对象
Demo* obj2 = new(raw_mem) Demo(20);
obj2->print();
obj2->~Demo();
// 释放原始内存
free(raw_mem);
return 0;
}
示例2:在栈内存上使用placement new
#include <iostream>
#include <new>
class StackDemo {
public:
StackDemo(const char* name) : name(name) {
std::cout << "栈对象构造:" << name << std::endl;
}
~StackDemo() {
std::cout << "栈对象析构:" << name << std::endl;
}
private:
const char* name;
};
int main() {
// 栈上分配一块足够大的内存,用alignas保证内存对齐符合要求
alignas(StackDemo) char stack_mem[sizeof(StackDemo)];
// 在栈内存上使用placement new构造对象
StackDemo* stack_obj = new(stack_mem) StackDemo("栈上对象");
// 手动调用析构函数,栈内存会在函数结束时自动释放,不需要额外操作
stack_obj->~StackDemo();
return 0;
}
使用placement new的注意事项
在使用placement new时,有几个关键点需要特别注意,否则很容易出现未定义行为:
- 内存大小必须足够:准备的内存空间大小必须至少等于目标对象的大小,否则构造对象时会越界访问内存。
- 内存对齐要匹配:不同平台、不同对象类型可能有不同的内存对齐要求,准备的内存地址必须满足目标对象的对齐规则,可以用alignas关键字或者std::aligned_storage来分配对齐的内存。
- 禁止用delete释放:placement new构造的对象绝对不能使用delete释放,因为原始内存不是它申请的,delete会尝试释放该地址,导致错误。必须手动调用析构函数,原始内存的释放由内存的分配方负责。
- 避免重复构造:同一块内存如果已经用placement new构造了对象,没有调用析构函数之前,不能再次用placement new构造同类型的对象,否则之前的对象的资源没有释放,会导致资源泄漏。
- 异常安全:如果placement new调用的构造函数抛出异常,已经分配的原始内存不会被自动释放,需要开发者手动处理异常场景下的内存和对象清理逻辑。
placement new和常规new的对比
为了更清晰地理解placement new的特点,我们可以将其和常规new做简单对比:
| 对比项 | 常规new | placement new |
|---|---|---|
| 内存申请 | 自动向堆申请内存 | 不申请内存,使用指定的已存在内存 |
| 构造函数调用 | 在申请的内存上调用构造函数 | 在指定的内存上调用构造函数 |
| 释放方式 | 用delete释放,会自动调用析构函数并释放内存 | 必须手动调用析构函数,原始内存由分配方释放 |
| 适用场景 | 常规的对象动态创建 | 特殊内存分配场景,如内存池、固定地址内存等 |
总的来说,placement new是C++中针对特殊内存分配场景设计的工具,它跳过了内存申请步骤,只完成对象构造的工作,给开发者提供了更灵活的内存操作能力,但也需要开发者自己承担更多的内存管理责任,使用时需要仔细核对内存大小、对齐规则以及对象的生命周期,避免出现内存相关的问题。
placement_newC++内存分配特殊内存场景修改时间:2026-06-09 20:54:38