new、delete 与 placement new 面试笔记
面试回答
常见问法
new和malloc有什么区别?delete和free有什么区别?placement new是什么?和普通new有什么不同?- 为什么
new失败默认抛异常,而不是返回空指针? - 为什么
new[]必须配delete[]? new/delete和operator new/operator delete是一回事吗?
回答
面试里这题最核心的点,不是背定义,而是把**“内存分配”和“对象构造”**拆开说清楚。
一句话先答
new:分配原始内存 + 在这块内存上构造对象delete:先析构对象 + 再释放原始内存malloc/free:只管理裸内存,不负责对象生命周期placement new:不分配内存,只在一块已存在的内存上构造对象
标准面试回答
new 不是 malloc 的语法糖。
它通常包含两步:
- 调用
operator new分配足够的原始内存 - 在这块内存上调用构造函数,完成对象初始化
delete 也不只是“释放内存”,它也有两步:
- 先调用对象析构函数
- 再调用
operator delete回收原始内存
而 malloc/free 只处理字节级内存,不知道对象、构造函数、析构函数、异常安全这些 C++ 对象模型的概念。
placement new 更特殊,它完全不申请内存,只是把对象“放到指定地址上构造出来”:
#include <new>
#include <string>
alignas(std::string) unsigned char buf[sizeof(std::string)];
std::string* p = new (buf) std::string("hello"); // 只构造,不分配
p->~basic_string(); // 需要手动析构
所以它的适用场景不是普通业务代码,而是:
- 对象池
- 预分配缓冲区
- 手写容器
- 自定义内存池
- 共享内存或 mmap 区域中的对象构造
怎么选
工程上一般这样选:
- 普通业务开发:几乎不直接写裸
new/delete,优先std::make_unique、std::make_shared、标准容器 - 需要动态对象,但不想手动管理生命周期:用智能指针
- 只想拿一块原始内存:极少数底层场景才会直接用
operator new或 allocator - 要在指定内存地址构造对象:用
placement new - C 风格内存块或与 C API 交互:才考虑
malloc/free
追问
1. 为什么 new 失败默认抛异常?
因为 C++ 更强调构造成功即得到有效对象。
如果失败返回空指针,调用者很容易忘记判空;而抛 std::bad_alloc 能把错误显式暴露出来,更符合异常机制和 RAII 风格。
如果确实想要“失败返回空指针”,可以用:
int* p = new (std::nothrow) int(42);
if (!p) {
// 处理分配失败
}
2. new[] 为什么必须配 delete[]?
因为数组对象的释放不仅是“还内存”,还涉及:
- 要对每个元素调用析构函数
- 运行时通常还需要知道数组元素个数
很多实现会在分配时额外保存数组长度等信息(常叫 array cookie)。
如果用错 delete,析构次数和释放逻辑都可能错,属于未定义行为。
3. placement new 后为什么不能直接 delete?
因为那块内存不是普通 new 分配的。
placement new 只负责构造对象,不拥有底层内存;所以销毁时只能:
- 显式调用析构函数
- 再由“真正拥有这块内存的人”去释放底层内存
例如缓冲区是栈上数组,就根本不需要释放;
如果缓冲区来自 ::operator new,那就最后再 ::operator delete。
4. new/delete 和 operator new/operator delete 有什么区别?
这是高频追问,最好主动说出来。
new/delete是表达式operator new/operator delete是函数
关系可以粗略理解为:
new T(args...)≈operator new(sizeof(T))+ 在该地址上调用T构造函数delete p≈ 调用析构函数 +operator delete(p)
所以:
operator new只管“拿内存”new才是“拿内存并构造对象”
5. malloc 分配对象内存后能直接当对象用吗?
不能直接当作“已构造对象”用。
malloc 只给你一块字节区,没有执行构造函数。
对于有非平凡构造函数的类型,必须再显式构造,通常就是 placement new。
6. delete nullptr 安全吗?
安全。
对空指针执行 delete 或 delete[] 没有问题,不会调用析构,也不会出错。
7. 基类指针 delete 派生类对象时要注意什么?
如果通过基类指针删除派生类对象,基类析构函数必须是虚函数,否则可能只调用基类析构,导致资源泄漏,属于未定义行为。
struct Base {
virtual ~Base() = default;
};
struct Derived : Base {
~Derived() { /* release resource */ }
};
8. 构造函数抛异常时,new 会不会泄漏内存?
正常的 new 表达式不会。
如果分配完内存后构造函数抛异常,运行时会自动调用对应的 operator delete 回收刚才分配的内存,这也是 new 比 malloc 更符合对象模型的一点。
原理展开
1. new/delete 到底做了什么
先看最基础模型:
#include <iostream>
struct A {
A() { std::cout << "ctor\n"; }
~A() { std::cout << "dtor\n"; }
};
int main() {
A* p = new A; // 分配 + 构造
delete p; // 析构 + 回收
}
面试要说到位,建议直接分层:
new T(args...) 的逻辑
- 计算
sizeof(T) - 调用
operator new(sizeof(T))获取原始内存 - 在这块内存上执行构造函数
- 返回类型正确的指针
delete p 的逻辑
- 判断指针是否为空
- 调用对象析构函数
- 调用
operator delete(p)释放原始内存
这也是为什么说:
new管的是“对象生命周期 + 内存”malloc管的只是“字节块”
2. new 和 malloc 的本质区别
2.1 是否理解“对象”
malloc 只知道要分配多少字节:
void* p = std::malloc(sizeof(std::string)); // 只是字节
这时你并没有得到一个真正构造完成的 std::string 对象。
而:
std::string* p = new std::string("hello");
你得到的是一个完整构造好的对象。
2.2 返回类型不同
malloc返回void*,C 中可隐式转换;C++ 中通常需要显式转换new返回目标类型指针,更安全,也更符合类型系统
2.3 失败处理不同
malloc失败返回nullptrnew默认抛std::bad_allocnew (std::nothrow)才返回nullptr
2.4 对初始化的支持不同
int* a = (int*)std::malloc(sizeof(int)); // 值未初始化
int* b = new int; // 默认初始化,内置类型仍可能未初始化
int* c = new int(); // 值初始化,得到 0
int* d = new int(42); // 直接初始化
C++ 的 new 与初始化语义深度绑定,这一点 malloc 做不到。
2.5 异常安全不同
如果对象构造过程可能抛异常,new 表达式会自动清理已分配内存;
而 malloc + 手动构造 需要你自己保证异常路径下不泄漏。
3. placement new 的本质
placement new 的形式是:
new (address) T(args...)
它表示:
在
address指向的那块现成内存上,直接构造一个T对象
注意:它不分配内存。
典型示例
#include <new>
#include <iostream>
struct A {
A(int x) : x(x) { std::cout << "ctor\n"; }
~A() { std::cout << "dtor\n"; }
int x;
};
int main() {
alignas(A) unsigned char storage[sizeof(A)];
A* p = new (storage) A(123); // 在指定内存上构造
std::cout << p->x << '\n';
p->~A(); // 手动析构
}
这里的 storage 是栈上缓冲区:
- 内存不是
new来的 - 所以不能
delete p - 只能显式析构对象
- 缓冲区生命周期由
storage自己决定
为什么一定要 alignas
对象有对齐要求。
如果地址不满足 T 的对齐要求,即使大小够,也可能是未定义行为。
正确写法:
alignas(T) unsigned char buf[sizeof(T)];
T* p = new (buf) T(...);
为什么要手动析构
因为 placement new 只是“构造对象”,没有“配套的 delete 表达式语义”。
也就是说,编译器不知道:
- 这块内存归谁管
- 什么时候该释放
- 该不该释放
所以对象销毁需要你自己控制:
p->~T();
4. placement new 的常见场景
4.1 对象池 / 内存池
底层先申请一大块内存,后续在固定槽位上反复构造和析构对象,减少频繁系统分配开销。
4.2 标准容器内部实现
像 vector 扩容时,本质上是:
- 申请一块更大的原始内存
- 在新内存上逐个构造元素
- 销毁旧元素
- 释放旧内存
这正是“分配”和“构造”分离的典型实践。
4.3 手写 Variant / Optional / Small Buffer
例如一个缓冲区里“可能放某种对象,也可能为空”,这类场景通常需要:
- 一块足够大的未初始化存储
- 真正需要时再构造对象
- 不需要时手动析构
4.4 共享内存 / mmap
底层内存来源不是普通堆,而是共享内存映射区域,这时只能在指定地址构造对象。
5. new[] / delete[] 为什么特殊
数组构造和单对象不同:
A* p = new A[3];
delete[] p;
这里运行时要做两件额外工作:
- 构造 3 个元素
- 删除时析构 3 个元素
很多实现为了正确析构数组,会在分配时额外保存元素个数。 这也是为什么:
A* p = new A[3];
delete p; // 错,未定义行为
不能混用。
6. new/delete 与 operator new/delete 的关系
这部分很容易拿加分。
6.1 operator new
它本质上更像“高级版 malloc”,负责分配原始内存:
void* raw = ::operator new(sizeof(int));
此时只是内存,还不是对象。
6.2 用 placement new 在原始内存上构造
int* p = new (raw) int(42);
6.3 显式析构 + 回收原始内存
p->~int(); // 对内置类型通常不这么写,这里只是示意
::operator delete(raw);
对类类型更典型:
#include <new>
#include <string>
void* raw = ::operator new(sizeof(std::string));
std::string* p = new (raw) std::string("hello");
p->~basic_string();
::operator delete(raw);
这段代码完整体现了对象生命周期四步:
- allocate
- construct
- destroy
- deallocate
7. 配对规则为什么不能错
下面这些都属于未定义行为:
int* p1 = new int(1);
std::free(p1); // 错
int* p2 = (int*)std::malloc(sizeof(int));
delete p2; // 错
A* p3 = new A[3];
delete p3; // 错
alignas(A) unsigned char buf[sizeof(A)];
A* p4 = new (buf) A();
delete p4; // 错
原因不是“风格不好”,而是生命周期模型根本不匹配:
- 谁分配的,不一定谁能释放
- 是否有析构动作,不一样
- 是否有数组元信息,不一样
- 运行时调用的释放函数,不一样
所以判断原则是:
谁分配的内存,就按它对应的规则释放;对象如何构造,就按对应方式销毁。
8. 工程实践怎么选
8.1 普通代码不要迷恋裸 new/delete
现代 C++ 中,绝大多数业务场景应该优先:
std::vectorstd::stringstd::unique_ptrstd::shared_ptrstd::make_uniquestd::make_shared
例如:
auto p = std::make_unique<std::string>("hello");
这比手写:
std::string* p = new std::string("hello");
delete p;
更安全,也更符合 RAII。
8.2 裸 new/delete 的合理场景
只在这些情况下还比较常见:
- 封装底层组件时需要自定义生命周期
- 实现容器、内存池、对象池
- 与旧代码或第三方接口适配
- 需要精细控制对象构造时机和内存布局
8.3 placement new 是底层工具,不是日常写法
它很强,但也很危险,因为你要自己负责:
- 对齐
- 析构
- 异常安全
- 重复构造/重复析构问题
- 类型别名和对象生存期规则
所以除非你明确在做底层组件,否则一般不会直接用它。
9. C++20 之后的工程补充
现代代码里,如果你只是想“在指定位置构造对象”,很多场景也会用:
std::construct_atstd::destroy_at- allocator /
allocator_traits
它们语义更清晰,更适合泛型代码。
例如:
#include <memory>
#include <new>
struct A {
A(int v) : v(v) {}
int v;
};
alignas(A) unsigned char storage[sizeof(A)];
A* p = std::construct_at(reinterpret_cast<A*>(storage), 42);
std::destroy_at(p);
底层思想和 placement new 一样,但接口更现代。
对比总结
| 概念 | 是否分配内存 | 是否构造对象 | 是否析构对象 | 是否释放内存 | 失败行为 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|---|---|---|---|
malloc | 是 | 否 | 否 | 否(需 free) | 返回 nullptr | C 接口、裸内存块 | 简单、通用 | 不懂对象模型,不安全 |
free | 否 | 否 | 否 | 是 | 无 | 释放 malloc/calloc/realloc 得到的内存 | 简单 | 不能管理 C++ 对象生命周期 |
new | 是 | 是 | 否 | 否(需 delete) | 默认抛 std::bad_alloc | 动态创建单个对象 | 类型安全、支持初始化和异常安全 | 裸用容易泄漏 |
delete | 否 | 否 | 是 | 是 | 安全处理空指针 | 销毁 new 创建的单个对象 | 生命周期完整 | 需严格配对 |
new[] | 是 | 是(多个) | 否 | 否(需 delete[]) | 默认抛异常 | 动态数组 | 支持逐元素构造 | 容易误配对 |
delete[] | 否 | 否 | 是(多个) | 是 | 安全处理空指针 | 销毁 new[] 创建的数组 | 正确析构所有元素 | 只能配 new[] |
operator new | 是 | 否 | 否 | 否(需 operator delete) | 默认抛异常 | 底层内存管理、自定义分配器 | 只拿原始内存,控制更细 | 不会构造对象 |
placement new | 否 | 是 | 否 | 否 | 取决于构造函数 | 对象池、容器实现、预分配缓冲区 | 可在指定地址原地构造 | 需手动析构,易出错 |
易错点
- 认为
new只是malloc的语法糖 - 把
new/delete和operator new/operator delete混为一谈 new[]后用delete,或者new后用delete[]malloc到一块内存后,直接把它当成“已构造对象”使用placement new后直接deleteplacement new时忽略对齐要求- 手动管理对象生命周期时,只构造不析构,或析构两次
- 通过基类指针删除派生类对象时,基类析构函数不是虚函数
- 在现代 C++ 中还到处手写裸
new/delete,而不是优先用 RAII/智能指针/容器
记忆技巧
-
两步模型一定要记住:
- allocate:拿原始内存
- construct:在内存上构造对象
- destroy:调用析构函数
- deallocate:归还原始内存
-
公式化记忆:
new= allocate + constructdelete= destroy + deallocateplacement new= only construct
-
配对原则:
new↔deletenew[]↔delete[]malloc↔freeoperator new↔operator deleteplacement new↔ 手动析构 + 底层内存按来源释放
-
面试答题口诀: 先讲对象生命周期,再讲内存来源,最后讲配对规则和工程实践。
面试速答版
new 和 malloc 最大区别在于,new 不只是申请内存,它还会在那块内存上调用构造函数;delete 也不只是释放内存,它会先调用析构函数再回收内存。malloc/free 只管理裸内存,不理解 C++ 对象生命周期。placement new 更特殊,它不分配内存,只在一块已存在的内存上原地构造对象,常用于对象池、容器实现和预分配缓冲区。工程上普通代码尽量不用裸 new/delete,优先智能指针和标准容器;placement new 属于底层工具,使用时要自己保证对齐、手动析构和正确释放底层内存。
面试加分版
这题我一般会把“内存分配”和“对象构造”拆开说。new 本质上不是 malloc 的语法糖,它做了两件事:先通过 operator new 分配原始内存,再在这块内存上调用构造函数,所以 new 得到的是一个真正构造完成的对象。delete 也分两步:先调用析构函数,再通过 operator delete 释放内存。相比之下,malloc/free 只处理字节块,不负责构造、析构、初始化和异常安全。
placement new 则更底层,它完全不申请内存,只是在一块已有内存上构造对象,比如 new (buf) T(args...)。这类写法适合对象池、预分配缓冲区、手写容器或者共享内存场景。因为底层内存不是普通 new 分配的,所以对象构造完以后不能直接 delete,而是要显式调用析构函数,然后由真正拥有那块内存的代码决定要不要释放底层缓冲区。
另外还有两个高频细节。第一,new 默认失败会抛 std::bad_alloc,这是为了让错误更显式,也更符合异常和 RAII 体系;如果想返回空指针,可以用 new (std::nothrow)。第二,new[] 一定要配 delete[],因为数组释放时要逐个调用元素析构,很多实现还会保存数组长度等额外信息,配错就是未定义行为。实际工程里,除非写底层组件,一般不推荐手写裸 new/delete,而是优先 std::make_unique、std::make_shared 和标准容器。