new、operator new、malloc、delete、operator delete
这题很容易把人问崩,因为很多人只会背:
new会调用构造malloc不会调用构造
但你答到语言层、运行库层、实现层。
你至少要回答这些:
- new 和 malloc的本质区别是什么
- 谁是运算符 / 表达式
- 谁是库函数
- new 做了哪几步
- 申请原始内存
- 类型转换
- 调用构造函数
- 失败时抛什么异常
- delete 做了哪几步
- 调用析构函数
- 释放原始内存
operator new和new表达式有什么区别operator delete和delete表达式有什么区别new[]和delete[]为什么必须配套- 为什么
new[]通常要额外记录元素个数 malloc得到的内存为什么不能直接拿来当复杂对象随便用- 构造函数如果抛异常,
new申请的内存会不会泄漏,为什么 - placement new 是什么,适合什么场景
下面开始追问,按真实面试官方式压你。
追问 1
请你说一下这两行代码到底发生了什么:
1 | A* p1 = new A; |
如果 A 里面有成员变量、虚函数、资源句柄,这两个对象有什么本质区别?
追问 2
new 表达式大致可以认为会被编译器拆成什么样?
你可以类似这样回答:
1 | void* mem = operator new(sizeof(A)); |
但你要解释清楚每一步的意义。
追问 3
为什么 new[] 和 delete[] 不能和 new / delete 混用?
不要只答“未定义行为”,你要解释:
- 编译器/运行库为什么需要知道数组里有多少个元素
- 为什么析构时要逐个析构
- 为什么单对象 delete 可能不知道该析构几个
追问 4
placement new 是“重新分配内存”吗?
比如:
1 | char buf[sizeof(A)]; |
这里到底发生了什么?
对象的生命周期从哪一刻开始?
之后该怎么销毁?能不能直接 delete p?
追问 5
类里可以重载 operator new 吗?有什么用途?
比如:
- 内存池
- 对齐分配
- 调试统计
- 限制对象只能堆上创建
你说一下实际工程意义。
再来一题小压轴追问,很多面试官喜欢问:
加问:delete this 能不能写?
你要回答:
- 什么情况下勉强可以
- 什么情况下绝对危险
- 为什么栈对象不能这么干
delete this之后为什么不能再访问任何成员
解析:
一、new 和 malloc 的本质区别
malloc 做的事情比较纯粹,它按字节申请一块原始内存,返回 void*。
这块内存里此时只是“有地方了”,但里面并没有真正构造出一个 C++ 对象。对于 int 这种平凡类型你可能感觉不明显,但对于 string、vector、自定义类这种带资源管理和不变式的对象,光有一块裸内存远远不够。
而 new 是一个完整的对象创建流程。它不只是拿内存,还会在那块内存上构造对象,所以 new T(...) 的结果是一个已经初始化完成的 T*,可以直接按对象语义使用。
所以一句话概括就是:
malloc 管的是内存,new 管的是对象。
二、谁是运算符,谁是库函数
这个最好明确说:
new/delete:是 C++ 语言层的表达式operator new/operator delete:是底层可重载的分配/释放函数malloc/free:是 C 标准库函数
也就是说,很多人把 new 和 operator new 混为一谈,其实不对。
三、new 做了哪几步
如果我写:
1 | T* p = new T(args...); |
这个 new 表达式从实现思路上,大致可以拆成这几步:
第一步:申请原始内存
它会先调用某个 operator new,申请一块足够容纳 T 的原始内存。
比如概念上类似:
1 | void* raw = operator new(sizeof(T)); |
这里拿到的是“未初始化原始存储区”。
第二步:把原始地址解释成对应类型的位置
这一步不是显式写出来的类型转换那种感觉,但你可以理解为:
编译器已经知道这里要放一个 T,所以接下来会在这块内存上构造 T 对象。
第三步:调用构造函数
在申请到的原始内存上执行构造,等价思想上接近 placement new:
1 | new(raw) T(args...); |
到这一步对象才真正诞生。
第四步:返回 T*
返回一个指向构造完成对象的指针。
失败时怎么办
默认的 operator new 分配失败时,不像 malloc 返回 NULL,而是会抛出:
1 | std::bad_alloc |
这个点面试里最好顺手说出来。
四、delete 做了哪几步
如果我写:
1 | delete p; |
它大致做两步:
第一步:调用析构函数
先执行对象的析构逻辑,把对象内部资源释放掉,比如文件句柄、堆内存、锁、连接等。
概念上类似:
1 | p->~T(); |
第二步:释放原始内存
析构结束后,再调用对应的 operator delete 把那块原始内存还回去。
也就是说:
析构负责“对象语义上的销毁”,operator delete 负责“内存层面的归还”。
这两个层次不要混。
五、operator new 和 new 表达式的区别
这是很容易追问的点。
new 表达式
是你平时写的:
1 | new T(args...) |
它是一个完整语义动作,包含:
- 分配内存
- 构造对象
- 失败处理
- 返回正确类型指针
operator new
只是一个函数,职责更像:
“给我一块指定大小的原始内存。”
它本身不调用构造函数。
所以你可以把它们的关系理解成:
new 表达式 = operator new + 在内存上构造对象
同理:
delete 表达式 = 调析构 + operator delete
六、operator delete 和 delete 表达式的区别
同样分层理解。
delete p
是完整销毁对象的语言表达式,先析构,再释放内存。
operator delete(p)
只是释放原始内存的函数,本身不会帮你调析构。
所以如果你绕开 delete,直接去调 operator delete,那对象内部资源清理逻辑可能根本没执行,通常是错误的。
七、new[] 和 delete[] 为什么必须配套
因为数组和单对象在销毁逻辑上不同。
1 | A* p = new A[10]; |
这里构造了 10 个对象。那销毁时必须知道:
- 要析构多少次
- 从哪个顺序析构
- 最后再释放整块内存
所以:
1 | delete[] p; |
会逐个调用 10 个元素的析构函数,然后再释放内存。
而如果你误写成:
1 | delete p; |
那编译器只会按“单个对象”语义处理,通常只析构一个元素,甚至释放逻辑也可能和分配方式不匹配,结果是未定义行为。
八、为什么 new[] 通常要额外记录元素个数
因为 delete[] 在运行时要知道“这个数组里有几个对象”,这样它才能循环析构。
但你传给 delete[] 的通常只是一个元素指针:
1 | A* p |
从这个指针本身并不能直接知道数组长度是多少。
所以很多实现会在用户可见数组首地址前面偷偷留一小段额外空间,记录元素个数,或者记录足以支持析构的信息。这一小段常被口语上叫做 array cookie。
这样 delete[] 才能在运行时回头找到数量信息,逐个调用析构函数。
你可以说:
new[] 通常不仅分配元素总大小,还会额外留元数据,供 delete[] 使用。
九、malloc 得到的内存为什么不能直接拿来当复杂对象随便用
因为 malloc 给你的只是字节块,不保证对象已经被构造。
比如:
1 | std::string* p = (std::string*)malloc(sizeof(std::string)); |
这里虽然类型转换后看起来是 std::string*,但那块内存上其实还没有真正构造出一个 std::string 对象。
它内部的指针、长度、容量等成员都没有通过构造函数建立正确状态。
你如果直接:
1 | p->size(); |
这就是未定义行为。
正确做法如果你真想用 malloc 拿原始内存,是还要再用 placement new 在这块内存上显式构造对象。
所以本质原因是:
“有内存”不等于“有对象”。
十、构造函数如果抛异常,new 申请的内存会不会泄漏
正常的 new 表达式不会泄漏,这个是很重要的点。
比如:
1 | T* p = new T(); |
如果第一步分配内存成功了,但第二步构造 T 时构造函数抛异常,那么这次对象创建没有完成。此时运行时会自动调用对应的 operator delete 把刚才申请的那块内存释放掉。
所以正常 new 表达式在“构造失败”这个场景下,语言机制会负责回收原始内存,不会因为构造函数抛异常而白白泄漏那块分配到的内存。
这个你可以理解成:
new 对“分配成功但构造失败”有成对清理机制。
当然,对象构造过程中如果你自己又申请了别的资源,但没用 RAII 管好,那是另一回事。
十一、placement new 是什么
placement new 是一种“在指定内存地址上构造对象”的机制。
比如:
1 | void* buf = malloc(sizeof(T)); |
这里不会再分配内存,它只是直接在 buf 指向的那块原始内存上调用 T 的构造函数。
所以 placement new 的核心作用不是分配,而是:
“在已有内存上显式构造对象。”
十二、placement new 适合什么场景
这个一般会用在比较底层、性能敏感或者需要手动管理对象生命周期的地方,比如:
1. 内存池 / 对象池
先批量申请大块内存,再在其中按需构造对象,减少频繁堆分配。
2. 容器底层实现
像 vector 扩容后拿到的是未初始化原始内存,然后会在对应位置上 placement new 构造元素。
3. 共享内存 / 预分配缓冲区
内存来源已经固定,但对象需要稍后按需初始化。
4. 手动控制对象生命周期
比如“内存先留着,对象晚点构造;对象先析构,但内存先不还”。
这里也要注意,placement new 构造出来的对象,销毁时通常要你自己显式调析构:
1 | p->~T(); |
因为普通 placement new 不负责自动释放那块外部提供的内存。
十三、在面试里会怎么收尾
我会总结成这样一段:
malloc/free 是 C 库层面的原始内存管理,只负责按字节申请和释放;new/delete 是 C++ 语言层的对象创建与销毁机制。new 表达式通常会先调用 operator new 申请原始内存,再在那块内存上调用构造函数,失败时默认抛 std::bad_alloc;delete 表达式则先调用析构函数,再调用 operator delete 释放原始内存。operator new/delete 本身只是底层分配释放函数,不等于 new/delete 表达式。new[]/delete[] 必须配套,是因为数组销毁时需要逐个析构元素,因此实现里通常还要额外记录元素个数。malloc 拿到的只是裸内存,不能直接当复杂对象用,因为对象还没构造。构造函数抛异常时,new 表达式会自动回收已经申请的内存。placement new 则适合在预先准备好的内存上显式构造对象,常用于内存池、容器底层和手动生命周期管理。
追问 3
new[] 和 delete[] 不能和 new / delete 混用,本质原因是单对象和数组对象的销毁协议不一样。数组删除时,运行库必须知道数组里到底有多少个元素,因为每个元素都是独立对象,析构时需要逐个调用析构函数,通常还是逆序析构。而用户手里通常只有首元素指针,光靠这个指针本身并不能知道数组长度,所以 new[] 的实现往往会额外记录元素个数等元数据,delete[] 再根据这些信息完成批量析构和整块内存释放。单对象 delete 按的是‘只析构一个对象’的协议,它通常不知道数组有几个元素,也不知道数组额外元数据的布局,因此如果拿它去释放 new[] 得到的指针,可能只析构一个元素,甚至连释放地址都可能错。同理,用 delete[] 去删单对象时,它又会误以为前面有数组元数据,从而读到错误信息并进行错误数量的析构。所以两者不能混用,不只是语法问题,而是运行时所需的对象数量和内存布局信息根本不同。
追问 5
类里是可以重载 operator new 的,它的作用是定制这个类对象在执行 new A 时的原始内存分配策略。也就是说,new 表达式本身还是先分配内存再调用构造函数,只不过‘分配内存’这一步不再走全局默认实现,而是优先走类自己的 operator new。工程里这样做很有实际意义。最典型的是内存池,对于频繁创建销毁的小对象,比如连接、任务节点、消息对象,可以从对象池中分配,减少系统堆分配开销和内存碎片;另外也可以用于特殊对齐分配,比如 SIMD 或缓存行对齐;还可以用于调试统计,记录某类对象的分配次数、存活数量、泄漏情况。至于限制对象只能堆上创建,通常不只是靠重载 operator new,还会配合私有析构、工厂方法或受控销毁接口来实现。总体来说,类内重载 `operator new`` 是对内存分配层的定制,适合在性能、对齐、调试或生命周期管理有明确需求时使用。
追问6
只有当这个对象百分之百是通过 new 创建出来的,并且你能百分之百保证执行 delete this 之后外部再也不会用它时,才勉强可以。
最典型的是这种“对象明确在堆上创建,而且生命周期就是自杀式结束”的设计。
比如引用计数对象、某些框架里的自管理对象:
1 | class A { |
这种场景勉强可以的前提有几个:
第一,这个对象必须保证是 new 出来的。
第二,调用 delete this 的函数执行后,调用者不能再使用这个对象。
第三,类的设计通常会把析构函数设成 private/protected,避免外部乱删。
第四,整个生命周期管理协议必须非常清晰,比如 COM 那种 AddRef/Release 风格。
也就是说,它不是“想写就写”,而是得放在一种非常受控的对象管理模型里。
什么情况下绝对危险
1)对象不是堆上创建的
比如:
1 | class A { |
这里 a 是栈对象。
它根本不是 new 分配出来的,你却 delete this,那就是拿 delete 去释放一块不属于堆分配协议的内存,直接未定义行为。
所以:
栈对象绝对不能 delete this。
2)对象可能是另一个对象的成员 / 嵌入子对象
比如:
1 | struct A { |
这里 B::a 是 B 对象内部的一个成员子对象,
它不是独立 new 出来的,也绝对不能 delete this。
因为 delete 适用于“独立动态分配出来的完整对象”,
不适用于成员子对象、数组元素、placement new 管理不当的对象等。
3)对象可能被多个地方继续访问
这个是最常见的危险点。
比如:
1 | A* p = new A(); |
destroy() 一旦执行了 delete this,那这个对象生命周期就结束了。
p 虽然这个指针变量还在,但它已经变成悬空指针。
后面再访问它,就是 use-after-free。
所以如果对象被很多地方持有,或者外部还以为它活着,delete this 非常危险。
三、为什么栈对象不能这么干
这个你面试里一定要讲清楚。
因为 delete 的语义不是“调用析构函数”这么简单,
它实际上会做两步:
第一步,调用析构函数。
第二步,调用对应的 operator delete 去释放原始内存。
问题就在于,栈对象的内存不是 operator new 分配出来的,
而是由编译器在栈帧里自动安排的。
比如:
1 | A a; |
这个 a 的内存属于当前函数栈帧的一部分。
它的释放方式不是 operator delete,而是函数返回时栈指针自动回退。
如果你对它 delete this,就相当于:
- 析构可能先执行一次
- 然后还错误地把一块栈内存当堆内存去释放
这当然是错的。
所以本质上不是“栈对象不能析构”,
而是:
栈对象的存储期和释放机制根本不归 delete 管。
四、delete this 之后为什么不能再访问任何成员
因为 delete this 一旦执行成功,当前对象的生命周期就已经结束了。
这意味着两件事:
1)对象已经析构了
成员变量可能已经被析构释放,内部资源可能已经没了。
2)对象占用的那块内存可能已经被回收
这块地址以后可能被别的对象重新分配使用。
所以这时候再访问:
- 成员变量
- 成员函数里继续用
this - 调用任何普通成员操作
本质上都是在访问一个已经死亡的对象,属于未定义行为。
例如:
1 | class A { |
这里 delete this 之后,x 所属对象已经没了。
你再去读 x,就是 use-after-free。
甚至连这样都不安全:
1 | void destroy() { |
因为调用普通成员函数本质上也要用到 this。
所以你可以直接背一句:
delete this 之后,this 立刻变成悬空指针,当前对象的生命周期已经结束,不能再访问任何成员,也不能再调用任何依赖该对象状态的成员函数。`
五、一个常见误区
有些人会说:
“那我 delete this 后马上 return 不就行了?”
这个说法只能说勉强减少风险,但前提还是很苛刻:
- 对象确实是堆对象
- 当前函数后面绝不再碰任何成员
- 外部调用方也知道这个对象已经死了
- 没有其他悬空引用/悬空指针
所以不是“return 了就安全”,
而是“满足严格生命周期约束时,delete this 后必须立刻结束当前使用路径”。