如何理解C++中的new运算符重载及placement new语法
你的理解存在部分偏差,我们从C++ new表达式的底层执行逻辑讲起,结合简单示例很容易搞懂。
所有new表达式都是两步执行
不管是普通的new int还是你写的带括号参数的new (x) int,C++编译器处理new表达式的时候永远拆成两个独立步骤执行:
- 第一步:内存分配阶段:编译器根据new后面括号里的参数,查找签名匹配的
operator new函数,传入「要创建的类型所占内存大小」+ 你在括号里写的所有额外参数,调用这个函数拿到一块可用的内存地址。你写的new (x) int确实会在这一步调用operator new(sizeof(int), x),但这只是整个流程的一半。 - 第二步:对象构造阶段:在第一步拿到的内存地址上,调用对应类型的构造函数(内置类型就按你写的初始化规则完成值初始化),最后返回类型匹配的指针。
你之前认为两种写法完全等价的错误点就在这里:单独调用
operator new(sizeof(int), x)只会返回一块内存地址,不会完成对象构造,直接访问这块内存的内容属于未定义行为。
最常见的放置式new用法示例
标准库自带了一个最常用的operator new重载:接收一个已经存在的内存地址,直接把这个地址原封不动返回,不做任何实际的内存分配。这也是很多人提到placement new最先想到的用法,常用来在预分配的内存(比如栈上缓冲区、内存池提前申请的大块内存)上构造对象,避免频繁向操作系统申请释放内存的开销:
#include <iostream> using namespace std; class MyObj { public: int val; MyObj(int v):val(v) { cout << "MyObj构造,val = " << val << endl; } ~MyObj() { cout << "MyObj析构,val = " << val << endl; } }; int main() { // 提前在栈上开一块足够存MyObj的内存,这时候内存上没有任何合法对象 char buf[sizeof(MyObj)]; // 放置式new:在buf对应的地址上构造MyObj对象,传123给构造函数 MyObj* p = new (buf) MyObj(123); // 验证:p的地址和buf首地址完全一致,对象可以正常使用 cout << "p指向地址:" << p << ",buf首地址:" << (void*)buf << endl; cout << "p->val = " << p->val << endl; // 注意:这里绝对不能写delete p!buf是栈上自动管理的内存,不是new申请的 // 只需要手动调用对象的析构函数即可 p->~MyObj(); return 0; }
这段代码的执行流程完全对应之前说的两步逻辑:
- 调用标准库内置的placement
operator new,传入sizeof(MyObj)和buf地址,函数直接返回buf的地址 - 在返回的地址上调用MyObj的构造函数,传入123完成初始化,最后返回
MyObj*类型的指针p
为什么C++要设计这种看起来奇怪的语法?
你有C#开发经验,觉得这种写法困惑很正常:C#的new是托管 runtime 封装死的,固定在托管堆分配内存+调用构造,开发者没有办法自定义内存分配的逻辑。但C++是手动管理内存的语言,放置式new的核心设计思路就是把「内存分配」和「对象构造」两个步骤完全解耦,带来了极高的灵活性:
- 你可以重载任意签名的
operator new,实现自定义内存分配逻辑:比如从固定大小内存池取内存、打印分配日志做内存泄漏检测、做内存越界填充校验等等,new后面括号里的内容就是传给你自定义operator new的参数 - 你可以在任意合法的、已经分配好的内存上构造对象,不需要每次创建对象都走系统调用申请内存,这也是高性能C++服务、游戏引擎里内存池实现的核心基础
举个简单的自定义operator new的例子,实现最基础的内存分配日志功能:
#include <iostream> #include <cstdlib> using namespace std; // 自定义operator new,额外接收文件名、行号参数用来打日志 void* operator new(size_t size, const char* file, int line) { cout << "申请" << size << "字节内存,位置:" << file << ":" << line << endl; return malloc(size); } // 必须写参数列表完全对应的placement operator delete,否则构造函数抛异常时会内存泄漏 void operator delete(void* ptr, const char* file, int line) { cout << "释放内存,位置:" << file << ":" << line << endl; free(ptr); } // 宏简化写法,很多C++项目的内存检测工具都是类似实现 #define NEW new(__FILE__, __LINE__) int main() { int* p = NEW int(999); cout << "*p = " << *p << endl; delete p; return 0; }
初学者容易踩的坑
- 只要实现了带自定义参数的
operator new,就必须实现参数列表完全匹配的operator delete,否则如果对象构造函数抛出异常,编译器找不到对应的释放函数,会直接造成内存泄漏 - 使用标准库自带的“传入预分配内存”的placement new时,绝对不要对返回的指针调用
delete:delete会先调用析构再释放内存,而那块内存不是operator new向系统申请的,释放不属于自己管理的内存会触发程序崩溃等未定义行为。这种场景下你只需要手动调用对象的析构函数,内存本身的生命周期由你自己管理(比如栈内存出作用域自动回收、内存池的内存统一还给池即可)
你直觉没错,C的new确实比托管语言里的new灵活很多,除了基础用法、放置式new,还要区分new表达式和operator new函数的区别、数组new的特殊逻辑等等,按顺序从基础到进阶练习很容易掌握。如果想系统性学习这块内容,可以按顺序先吃透普通new/delete的基础用法,再理解C运算符重载规则,最后专门练习placement new的使用场景,就能完全理清相关知识点。
内容的提问来源于stack exchange,提问作者Vincent Mazzarella

