std::make_shared只分配一次内存,因为它将控制块和对象置于同一块连续内存中;而手动构造shared_ptr需分别分配对象和控制块,至少两次堆分配。

std::make_shared 为什么只分配一次内存
因为 std::make_shared 把控制块(control block)和对象本身放在同一块连续内存里,而手动用 new 构造 std::shared_ptr 时,控制块和对象是分开分配的——至少两次堆分配。
- 控制块存引用计数、弱引用计数、删除器等元数据,必须存在,但位置可选
-
std::make_shared<t>(args...)</t>内部调用operator new(sizeof(control_block) + sizeof(T))一次性申请 - 手动写
std::shared_ptr<t>(new T(args...))</t>:先一次分配T,再一次分配控制块,共两次malloc/堆操作 - 在频繁创建小对象(如
std::shared_ptr<int></int>)时,这差异会被放大
哪些情况会让 std::make_shared 失效甚至更慢
不是所有构造场景都适合 std::make_shared;它依赖完美转发 + placement new,一旦构造过程绕过它,优势就没了,甚至引发额外开销。
- 类定义了私有或删除的
operator new:std::make_shared无法调用自定义分配器,可能编译失败或退回到默认分配 - 需要自定义删除器(比如用
close关文件描述符):不能直接用std::make_shared,得走std::shared_ptr<t>(new T, deleter)</t>,此时又变回两次分配 - 类没有 public 构造函数,或构造逻辑依赖外部状态(如需先调用
init()),std::make_shared无法介入,只能手动 new - 某些 STL 实现(如旧版 libstdc++)对数组类型支持不完整:
std::make_shared<int>()</int>可能不工作,而new int[10]是合法的
性能差异实际有多大?看典型场景
差异主要体现在分配频次高、对象小、无锁竞争低的场景;真实服务中是否可观测,取决于你的热点路径。
- 在空循环里创建 100 万个
std::shared_ptr<int></int>:用std::make_shared比手动new快约 15%–25%,主要是减少 malloc 调用次数和缓存局部性更好 - 对象较大(如
std::shared_ptr<:vector>></:vector>占几百字节):分配次数仍是两次 vs 一次,但相对占比下降,性能差通常缩到 5% 以内 - 启用了 jemalloc 或 mimalloc 等优化分配器时,多次小分配的开销被摊薄,
std::make_shared的收益会减弱,但内存布局优势仍在 - 注意:如果对象构造函数抛异常,
std::make_shared能保证控制块和对象内存一并释放,不会泄漏——这点常被忽略,但它不是“性能”优势,而是安全底线
std::make_shared 不支持的构造方式怎么补救
当必须绕过 std::make_shared 时,别硬套;优先确保语义正确,再考虑是否值得为那点分配开销手写定制分配器。
立即学习“C++免费学习笔记(深入)”;
- 要自定义删除器:老实用
std::shared_ptr<t>(new T(args...), [](T* p) { /* custom cleanup */ })</t> - 要使用非默认分配器(如
std::pmr::polymorphic_allocator):C++17 起可用std::allocate_shared,传入分配器对象,它也能做到单次分配 - 类不可移动/不可拷贝且构造参数复杂:检查是否真需要
shared_ptr——有时std::unique_ptr+std::move更轻量,也避免控制块开销 - 跨 DLL 边界传递
shared_ptr(Windows 上常见):控制块分配器必须一致,否则析构崩溃;这时往往要强制用同一 allocator,std::make_shared反而难控制,不如显式管理
真正容易被忽略的是:控制块和对象共享内存虽省了一次分配,但也意味着只要还有 weak_ptr 活着,对象内存就不能回收——哪怕 shared_ptr 已全销毁。这个生命周期耦合,在调试内存占用时经常让人困惑。










