new、operator new、malloc、delete、operator delete

new、operator new、malloc、delete、operator delete

这题很容易把人问崩,因为很多人只会背:

  • new 会调用构造
  • malloc 不会调用构造

但你答到语言层、运行库层、实现层

你至少要回答这些:

  1. new 和 malloc的本质区别是什么
    • 谁是运算符 / 表达式
    • 谁是库函数
  2. new 做了哪几步
    • 申请原始内存
    • 类型转换
    • 调用构造函数
    • 失败时抛什么异常
  3. delete 做了哪几步
    • 调用析构函数
    • 释放原始内存
  4. operator newnew 表达式有什么区别
  5. operator deletedelete 表达式有什么区别
  6. new[]delete[] 为什么必须配套
  7. 为什么 new[] 通常要额外记录元素个数
  8. malloc 得到的内存为什么不能直接拿来当复杂对象随便用
  9. 构造函数如果抛异常,new 申请的内存会不会泄漏,为什么
  10. placement new 是什么,适合什么场景

下面开始追问,按真实面试官方式压你。

追问 1

请你说一下这两行代码到底发生了什么:

1
2
A* p1 = new A;
A* p2 = (A*)malloc(sizeof(A));

如果 A 里面有成员变量、虚函数、资源句柄,这两个对象有什么本质区别?


追问 2

new 表达式大致可以认为会被编译器拆成什么样?

你可以类似这样回答:

1
2
3
4
5
6
7
void* mem = operator new(sizeof(A));
try {
A* p = new(mem) A();
} catch (...) {
operator delete(mem);
throw;
}

但你要解释清楚每一步的意义。


追问 3

为什么 new[]delete[] 不能和 new / delete 混用?

不要只答“未定义行为”,你要解释:

  • 编译器/运行库为什么需要知道数组里有多少个元素
  • 为什么析构时要逐个析构
  • 为什么单对象 delete 可能不知道该析构几个

追问 4

placement new 是“重新分配内存”吗?

比如:

1
2
char buf[sizeof(A)];
A* p = new(buf) A();

这里到底发生了什么?
对象的生命周期从哪一刻开始?
之后该怎么销毁?能不能直接 delete p


追问 5

类里可以重载 operator new 吗?有什么用途?

比如:

  • 内存池
  • 对齐分配
  • 调试统计
  • 限制对象只能堆上创建

你说一下实际工程意义。


再来一题小压轴追问,很多面试官喜欢问:

加问:delete this 能不能写?

你要回答:

  1. 什么情况下勉强可以
  2. 什么情况下绝对危险
  3. 为什么栈对象不能这么干
  4. delete this 之后为什么不能再访问任何成员

解析:

一、new 和 malloc 的本质区别

malloc 做的事情比较纯粹,它按字节申请一块原始内存,返回 void*
这块内存里此时只是“有地方了”,但里面并没有真正构造出一个 C++ 对象。对于 int 这种平凡类型你可能感觉不明显,但对于 stringvector、自定义类这种带资源管理和不变式的对象,光有一块裸内存远远不够。

new 是一个完整的对象创建流程。它不只是拿内存,还会在那块内存上构造对象,所以 new T(...) 的结果是一个已经初始化完成的 T*,可以直接按对象语义使用。

所以一句话概括就是:

malloc 管的是内存,new 管的是对象。


二、谁是运算符,谁是库函数

这个最好明确说:

  • new / delete:是 C++ 语言层的表达式
  • operator new / operator delete:是底层可重载的分配/释放函数
  • malloc / free:是 C 标准库函数

也就是说,很多人把 newoperator 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
2
void* buf = malloc(sizeof(T));
T* p = new(buf) T(args...);

这里不会再分配内存,它只是直接在 buf 指向的那块原始内存上调用 T 的构造函数。

所以 placement new 的核心作用不是分配,而是:

“在已有内存上显式构造对象。”


十二、placement new 适合什么场景

这个一般会用在比较底层、性能敏感或者需要手动管理对象生命周期的地方,比如:

1. 内存池 / 对象池

先批量申请大块内存,再在其中按需构造对象,减少频繁堆分配。

2. 容器底层实现

vector 扩容后拿到的是未初始化原始内存,然后会在对应位置上 placement new 构造元素。

3. 共享内存 / 预分配缓冲区

内存来源已经固定,但对象需要稍后按需初始化。

4. 手动控制对象生命周期

比如“内存先留着,对象晚点构造;对象先析构,但内存先不还”。

这里也要注意,placement new 构造出来的对象,销毁时通常要你自己显式调析构:

1
2
p->~T();
free(buf);

因为普通 placement new 不负责自动释放那块外部提供的内存。


十三、在面试里会怎么收尾

我会总结成这样一段:

malloc/free 是 C 库层面的原始内存管理,只负责按字节申请和释放;new/delete 是 C++ 语言层的对象创建与销毁机制。new 表达式通常会先调用 operator new 申请原始内存,再在那块内存上调用构造函数,失败时默认抛 std::bad_allocdelete 表达式则先调用析构函数,再调用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
public:
void release() {
if (--ref_cnt == 0) {
delete this;
}
}

private:
int ref_cnt{1};

~A() {
// 私有析构,防止外部乱 delete
}
};

这种场景勉强可以的前提有几个:

第一,这个对象必须保证是 new 出来的
第二,调用 delete this 的函数执行后,调用者不能再使用这个对象
第三,类的设计通常会把析构函数设成 private/protected,避免外部乱删。
第四,整个生命周期管理协议必须非常清晰,比如 COM 那种 AddRef/Release 风格。

也就是说,它不是“想写就写”,而是得放在一种非常受控的对象管理模型里。


什么情况下绝对危险

1)对象不是堆上创建的

比如:

1
2
3
4
5
6
7
8
9
10
11
class A {
public:
void destroy() {
delete this;
}
};

int main() {
A a;
a.destroy();
}

这里 a栈对象
它根本不是 new 分配出来的,你却 delete this,那就是拿 delete 去释放一块不属于堆分配协议的内存,直接未定义行为。

所以:

栈对象绝对不能 delete this


2)对象可能是另一个对象的成员 / 嵌入子对象

比如:

1
2
3
4
5
6
7
struct A {
void destroy() { delete this; }
};

struct B {
A a;
};

这里 B::aB 对象内部的一个成员子对象,
它不是独立 new 出来的,也绝对不能 delete this

因为 delete 适用于“独立动态分配出来的完整对象”,
不适用于成员子对象、数组元素、placement new 管理不当的对象等。


3)对象可能被多个地方继续访问

这个是最常见的危险点。

比如:

1
2
3
A* p = new A();
p->destroy(); // 里面 delete this
p->foo(); // 炸

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
2
3
4
5
6
7
8
9
10
class A {
public:
void destroy() {
delete this;
std::cout << x << std::endl; // 错
}

private:
int x = 42;
};

这里 delete this 之后,x 所属对象已经没了。
你再去读 x,就是 use-after-free。

甚至连这样都不安全:

1
2
3
4
void destroy() {
delete this;
foo(); // 也不行
}

因为调用普通成员函数本质上也要用到 this

所以你可以直接背一句:

delete this 之后,this 立刻变成悬空指针,当前对象的生命周期已经结束,不能再访问任何成员,也不能再调用任何依赖该对象状态的成员函数。`


五、一个常见误区

有些人会说:

“那我 delete this 后马上 return 不就行了?”

这个说法只能说勉强减少风险,但前提还是很苛刻:

  • 对象确实是堆对象
  • 当前函数后面绝不再碰任何成员
  • 外部调用方也知道这个对象已经死了
  • 没有其他悬空引用/悬空指针

所以不是“return 了就安全”,
而是“满足严格生命周期约束时,delete this 后必须立刻结束当前使用路径”。