⚡C++ 内存管理

new、delete 与 placement new 面试笔记

面试回答

常见问法

  • newmalloc 有什么区别?
  • deletefree 有什么区别?
  • placement new 是什么?和普通 new 有什么不同?
  • 为什么 new 失败默认抛异常,而不是返回空指针?
  • 为什么 new[] 必须配 delete[]
  • new/deleteoperator new/operator delete 是一回事吗?

回答

面试里这题最核心的点,不是背定义,而是把**“内存分配”“对象构造”**拆开说清楚。

一句话先答

  • new分配原始内存 + 在这块内存上构造对象
  • delete先析构对象 + 再释放原始内存
  • malloc/free只管理裸内存,不负责对象生命周期
  • placement new不分配内存,只在一块已存在的内存上构造对象

标准面试回答

new 不是 malloc 的语法糖。 它通常包含两步:

  1. 调用 operator new 分配足够的原始内存
  2. 在这块内存上调用构造函数,完成对象初始化

delete 也不只是“释放内存”,它也有两步:

  1. 先调用对象析构函数
  2. 再调用 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_uniquestd::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 只负责构造对象,不拥有底层内存;所以销毁时只能:

  1. 显式调用析构函数
  2. 再由“真正拥有这块内存的人”去释放底层内存

例如缓冲区是栈上数组,就根本不需要释放; 如果缓冲区来自 ::operator new,那就最后再 ::operator delete


4. new/deleteoperator 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 安全吗?

安全。 对空指针执行 deletedelete[] 没有问题,不会调用析构,也不会出错。


7. 基类指针 delete 派生类对象时要注意什么?

如果通过基类指针删除派生类对象,基类析构函数必须是虚函数,否则可能只调用基类析构,导致资源泄漏,属于未定义行为。

struct Base {
    virtual ~Base() = default;
};
struct Derived : Base {
    ~Derived() { /* release resource */ }
};

8. 构造函数抛异常时,new 会不会泄漏内存?

正常的 new 表达式不会。 如果分配完内存后构造函数抛异常,运行时会自动调用对应的 operator delete 回收刚才分配的内存,这也是 newmalloc 更符合对象模型的一点。


原理展开

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...) 的逻辑

  1. 计算 sizeof(T)
  2. 调用 operator new(sizeof(T)) 获取原始内存
  3. 在这块内存上执行构造函数
  4. 返回类型正确的指针

delete p 的逻辑

  1. 判断指针是否为空
  2. 调用对象析构函数
  3. 调用 operator delete(p) 释放原始内存

这也是为什么说:

new 管的是“对象生命周期 + 内存” malloc 管的只是“字节块”


2. newmalloc 的本质区别

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 失败返回 nullptr
  • new 默认抛 std::bad_alloc
  • new (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 扩容时,本质上是:

  1. 申请一块更大的原始内存
  2. 在新内存上逐个构造元素
  3. 销毁旧元素
  4. 释放旧内存

这正是“分配”和“构造”分离的典型实践。

4.3 手写 Variant / Optional / Small Buffer

例如一个缓冲区里“可能放某种对象,也可能为空”,这类场景通常需要:

  • 一块足够大的未初始化存储
  • 真正需要时再构造对象
  • 不需要时手动析构

4.4 共享内存 / mmap

底层内存来源不是普通堆,而是共享内存映射区域,这时只能在指定地址构造对象。


5. new[] / delete[] 为什么特殊

数组构造和单对象不同:

A* p = new A[3];
delete[] p;

这里运行时要做两件额外工作:

  1. 构造 3 个元素
  2. 删除时析构 3 个元素

很多实现为了正确析构数组,会在分配时额外保存元素个数。 这也是为什么:

A* p = new A[3];
delete p;   // 错,未定义行为

不能混用。


6. new/deleteoperator 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);

这段代码完整体现了对象生命周期四步:

  1. allocate
  2. construct
  3. destroy
  4. 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::vector
  • std::string
  • std::unique_ptr
  • std::shared_ptr
  • std::make_unique
  • std::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_at
  • std::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返回 nullptrC 接口、裸内存块简单、通用不懂对象模型,不安全
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/deleteoperator new/operator delete 混为一谈
  • new[] 后用 delete,或者 new 后用 delete[]
  • malloc 到一块内存后,直接把它当成“已构造对象”使用
  • placement new 后直接 delete
  • placement new 时忽略对齐要求
  • 手动管理对象生命周期时,只构造不析构,或析构两次
  • 通过基类指针删除派生类对象时,基类析构函数不是虚函数
  • 在现代 C++ 中还到处手写裸 new/delete,而不是优先用 RAII/智能指针/容器

记忆技巧

  • 两步模型一定要记住:

    • allocate:拿原始内存
    • construct:在内存上构造对象
    • destroy:调用析构函数
    • deallocate:归还原始内存
  • 公式化记忆:

    • new = allocate + construct
    • delete = destroy + deallocate
    • placement new = only construct
  • 配对原则:

    • newdelete
    • new[]delete[]
    • mallocfree
    • operator newoperator delete
    • placement new手动析构 + 底层内存按来源释放
  • 面试答题口诀: 先讲对象生命周期,再讲内存来源,最后讲配对规则和工程实践。


面试速答版

newmalloc 最大区别在于,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_uniquestd::make_shared 和标准容器。