std::make_shared只分配一次内存,因为它将控制块与对象合并分配;而直接new再构造shared_ptr需至少两次分配,且有异常泄漏风险,但不支持自定义删除器或分配器。

std::make_shared 为什么只分配一次内存
因为 std::make_shared 把控制块(control block)和对象本身合并在同一块动态内存里分配,而直接用 new 构造再传给 std::shared_ptr 构造函数时,控制块和对象是两次独立分配。
控制块里存引用计数、弱引用计数、删除器等元数据;对象是用户数据。分开分配意味着至少两次 operator new 调用,还可能跨缓存行,影响局部性。
- 典型场景下:用
std::make_shared<int>(42)</int>→ 1 次分配 - 等价写法
std::shared_ptr<int>(new int(42))</int>→ 至少 2 次分配(对象 + 控制块) - 若自定义删除器或分配器,次数可能更多
异常安全问题:构造函数抛异常时的资源泄漏风险
直接使用 new 构造对象再传给 std::shared_ptr,在对象构造过程中若抛异常,new 分配的内存不会被自动释放——因为 shared_ptr 构造还没开始,也就没机会接管资源。
std::make_shared 将对象构造和控制块创建封装在原子操作中:要么全部成功,要么全部失败(控制块和对象的内存会在异常路径中一并释放)。
- 危险写法:
std::shared_ptr<t>(new T(args...))</t>—— 若T的构造函数抛异常,new T的内存泄露 - 安全写法:
std::make_shared<t>(args...)</t>—— 异常发生时,内部调用的operator new分配会回滚,无泄漏 - 注意:如果
args...中的表达式自身抛异常(比如某个参数是函数调用),那还没到对象构造阶段,也不涉及内存泄漏,但仍是未定义行为前的潜在风险点
不支持自定义删除器和分配器的限制
std::make_shared 无法指定自定义删除器或分配器,这是它和裸 shared_ptr 构造函数的关键差异。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
原因在于:控制块与对象内存布局是紧耦合的,一旦引入外部分配器或删除逻辑,就难以统一管理生命周期。标准库选择牺牲灵活性来换取内存效率和异常安全性。
- 需要自定义删除器?只能用
std::shared_ptr<t>(new T, my_deleter)</t>或std::shared_ptr<t>(ptr, deleter, alloc)</t> - 需要自定义分配器?必须绕过
make_shared,改用std::allocate_shared(C++11 起提供) -
std::allocate_shared支持分配器,但仍不支持自定义删除器(删除器仍由控制块管理)
移动语义与完美转发的实际表现
std::make_shared 内部使用完美转发,能保留参数的左/右值属性,避免不必要的拷贝;但它对移动语义的利用受限于对象本身的构造函数签名。
例如:若 T 的构造函数接受 const T&,即使你传入一个右值,也会触发拷贝而非移动——这不是 make_shared 的问题,而是 T 的设计问题。
- 推荐检查
T是否提供了移动构造函数,并确认参数类型是否匹配(如用T&&或std::string&&) - 不要假设“用了
make_shared就一定移动”,关键看实参和形参的绑定方式 - 调试时可加日志或断点验证:是否真的调用了移动构造而非拷贝构造
真正容易被忽略的是:控制块和对象共享内存虽高效,但也导致对象无法单独析构——只要还有 weak_ptr 存活,控制块就得一直留着,哪怕对象早已销毁。这意味着内存不能立即返还给系统,尤其在大量短生命周期对象 + 长期存活 weak_ptr 的场景下,会放大内存占用延迟。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!









