资讯动态

operator new 和 placement new

发布时间:2026/10/1 23:45:21 来源:尧图企业网站定制
C 动态内存管理深度解析从operator new到 placement new一、概念正本清源new 表达式 vsoperator new函数很多初学者把 new 当作一个不可分割的整体但实际上 C 把动态内存的两个职责拆得很开步骤new 表达式 (new T(...))delete 表达式 (delete p)1调用operator new申请内存调用析构函数销毁对象2在内存上调用构造函数调用operator delete释放内存重要事实和operator这类运算符不同operator new/operator delete不能重载 new/delete 表达式本身——你只能改变其中分配/释放内存那一步不能改变 new 表达式先分配再构造、delete 表达式先析构再释放这一固定语义。Widget* p new Widget(42);// 等价于void* mem operator new(sizeof(Widget));Widget* p new (mem) Widget(42); // placement new 只构造二、operator new/operator delete定制规则2.1 作用域要求必须定义在 全局作用域 或 类作用域。当作为类成员定义时它们是 隐式静态的implicit static。原因很自然operator new在对象构造之前调用 → 此时对象尚不存在没有this。operator delete在对象销毁之后调用 → 此时对象已不存在没有this。2.2 签名要求// operator newvoid* operator new(size_t size); // 单对象void* operator new[](size_t size); // 数组// 第一个参数必须是 size_t且不能有默认实参// 返回类型必须是 void*// operator deletevoid operator delete(void* p) noexcept; // 单对象void operator delete[](void* p) noexcept; // 数组// 第一个参数必须是 void*// 返回类型必须是 void类成员版本的operator delete还可以多接收一个size_t参数初值为指针所指对象的字节数——这在销毁继承体系中带虚析构函数的对象时特别有用实际字节数取决于动态类型。2.3 一个简单的实现基于malloc/freeC 从 C 继承了malloc/free位于cstdlib它们是搭建自定义operator new的基石void* operator new(size_t size) {if (void* mem std::malloc(size))return mem;throw std::bad_alloc();}void operator delete(void* mem) noexcept {std::free(mem);}三、唯一不可重载的特殊形式void* operator new(size_t, void*); // 标准库保留禁止用户重载这就是 placement new 函数。它的标准实现极其简单void* operator new(size_t, void* p) noexcept { return p; }它不分配任何内存只是把传入的指针p原封不动返还让 new 表达式的下一步——构造函数——在p指向的内存上就地构造。四、零长数组合法但特殊的new T[0]char arr[0]; // 错误不能定义 0 长度数组char* cp new char[0]; // 合法但 *cp 不能解引用new T[0]返回的指针有几个明确保证非空 且 唯一不与任何其他new返回的指针相等。行为类似 过尾指针 / off-the-end iterator可以参与比较for (p; p ! end; p)自然终止。可以加 0、减 0自身相减得 0。不能解引用——它不指向任何元素。这一设计让 用一对指针描述一个空区间 的写法天然成立无需特判n 0。五、Placement New 详解5.1 它是什么Placement new 是operator new的一个标准全局重载版本其原型为void* operator new(size_t, void* p) noexcept { return p; }它允许我们在一段已经分配好的内存无论在堆还是在栈上构造新对象。5.2 为什么需要它普通new需要在堆上查找足够大的空闲块这有两个代价耗时不确定堆查找速度慢且不可预测。可能失败内存不足会抛std::bad_alloc。Placement new 把分配和构造彻底解耦内存可预先分配并反复复用避免碎片。构造时间是 常数不会中途内存不足。非常适合实时性要求高、运行不希望被打断的场景嵌入式、L1/L2 实时调度、内存池等。5.3 标准五步用法class Foo {char cc;float f;public:void print() { std::cout ADDR: this \n; }void set_f(float v){ f v; }void get_f() const { std::cout f f \n; }};// 1) 分配内存堆或栈均可char* buff new char[sizeof(Foo) * N];std::memset(buff, 0, sizeof(Foo) * N);// 2) 在已分配内存上构造对象这一步才是 placement newFoo* pfoo new (buff) Foo;// 3) 使用对象pfoo-print();pfoo-set_f(1.0f);pfoo-get_f();// 4) 显式调用析构函数关键placement new 没分配内存// 所以不能用 delete pfoo——那会去释放 buff 之外的东西pfoo-~Foo();// 5) 释放最初分配的内存与第 1 步对应delete[] buff; // 若 buff 在栈上则跳出作用域自动释放5.4 数组形式的隐含开销C 标准对placement operator new[]有特别说明placement operator new[] needs implementation-defined amount of additional storage to save a size of array.也就是说placement new 数组版需要额外的几个字节实现定义通常为sizeof(size_t)来记录数组元素个数。如果你打算用 placement new 创建 N 个对象的数组必须为这个隐藏的长度头也预留空间char* buf new char[N * sizeof(MyClass) sizeof(int)];MyClass* arr new (buf) MyClass[N];六、案例剖析auto var *new Widget(42); delete var;源代码关键路径已略去打印细节class Widget {int value{};public:Widget(int v 10) : value{v} { std::cout Widget(int) constructor\n; }~Widget() {if (value 0) std::cout 0~destructor: this \n;else { value 0; std::cout ~destructor: this \n; }}void print() { std::cout value: value : this \n; }// ... 拷贝/移动构造、赋值省略};int main() {auto var *new Widget(42);var.print();delete var;}6.1 逐句拆解表达式行为输出new Widget(42)①operator new分配sizeof(Widget)字节② 在该内存上调用Widget(int)Widget(int) constructor*new Widget(42)解引用new返回的指针得到 左值Widget—auto var *...引用绑定到那块堆内存上的对象没有发生任何拷贝—var.print()普通成员调用value:42 : 0xADDRvar取引用绑定对象的地址就是new返回的原指针—delete var① 调用~Widget()②operator delete释放该指针指向的内存~destructor:0xADDR6.2 是否泄漏——关键洞察很多人误以为 用引用接住*new一定会泄漏因为指针丢了。这是 不对的var会精确地恢复出new返回的地址——只要var是直接绑定到*new T(...)这种刚出炉的对象var与原始指针二进制完全相同。因此delete var等同于delete p析构和释放都会被正确触发。真正的风险不在于泄漏而在于可读性与可维护性类型不再表达所有权——读者很难一眼看出var背后是堆对象。异常安全极差——若var.print()抛异常delete var永远执行不到立刻泄漏。多重继承下var的地址可能与new返回的指针不同涉及指针调整此时delete var会触发未定义行为。结论上述写法在语法上合法、在简单场景下不泄漏但属于反模式。生产代码应当使用std::unique_ptrWidget或值语义栈上对象。6.3 预期完整输出Widget(int) constructorvalue:42 : 0xADDR~destructor:0xADDR七、易错点速查表误区真相重载 new 表达式不可能只能重载operator new这一步void* operator new(size_t, void*)自定义非法标准库保留new T[0]报错合法返回唯一非空但不可解引用的指针placement new 后用delete错误必须显式调用析构再用对应方式释放原始内存auto v *new T; delete v;一定泄漏简单场景下不会但在多重继承、异常路径下极易出问题malloc出来的内存可以delete未定义行为必须malloc/free、new/delete严格成对八、给工程实践的建议优先使用 RAIIstd::unique_ptr/std::shared_ptr/ 容器 / 栈对象 ——几乎可以消除手写new/delete的需求。真要自定义内存管理时成对实现operator new与operator delete并保持类成员版本与基类签名匹配避免切片释放失败。内存池场景用 placement new 在预分配的 buffer 上反复构造/析构构造时间常数化杜绝运行期内存不足。严守对称原则new↔deletenew[]↔delete[]placement new↔ 显式析构 原 buffer 的对应释放方式malloc↔free掌握了new 表达式 operator new 构造函数这个分解几乎所有 C 动态内存的奇怪行为都会变得自然——包括 placement new、零长数组、自定义分配器、内存池乃至std::vector内部如何把容量与大小解耦。这是从会用 new 走向理解内存模型的关键一步。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑