make_shared 更高效,因其一次内存分配同时构建控制块和对象,避免了new+shared_ptr的两次分配及数据局部性差的问题,并具备强异常安全保证。

make_shared 为什么比 new + shared_ptr 构造更高效
make_shared 的核心优势在于「一次内存分配完成控制块和对象的布局」,而手动用 new 构造再传给 shared_ptr 需要两次分配:一次给对象,一次给控制块(含引用计数、弱引用计数等)。这不仅多一次系统调用开销,还破坏了数据局部性——对象和它的控制块可能分散在不同内存页。
典型反例:
auto p1 = std::shared_ptr<int>(new int(42)); // 两次分配
对应等价但高效的写法:
auto p2 = std::make_shared<int>(42); // 一次分配,对象紧贴控制块之后
控制块内存布局差异直接影响 cache 命中率
标准库实现(如 libstdc++ 和 libc++)中,make_shared 分配的内存块结构通常是:control_block_header + aligned_storage_for<t></t>。这意味着访问 shared_ptr::get() 指向的对象时,控制块往往已在同一 cache line 中——尤其在频繁拷贝、析构或调用 use_count() 的场景下,减少 cache miss 效果明显。
立即学习“C++免费学习笔记(深入)”;
手动构造则无法保证这种布局,控制块和对象地址差可能达 KB 级别。
- 若对象很小(如
int、std::string小字符串优化态),make_shared节省的分配次数和提升的局部性收益最显著 - 若类型重载了
operator new,make_shared仍会调用全局::operator new分配整块内存,不会触发类特化版本——这点常被忽略 - 不支持直接传递自定义删除器(
make_shared固定使用default_delete);需自定义删除器时只能退回到shared_ptr构造函数
make_shared 对异常安全的隐式保障
当构造函数可能抛异常时,make_shared<t>(args...)</t> 是强异常安全的:要么完整构造成功并返回 shared_ptr,要么不分配、不调用构造函数、不泄漏资源。而手动写法存在隐患:
auto p = std::shared_ptr<T>(new T(a, b, c)); // 若 T(a,b,c) 抛异常,new 已分配内存但未被 shared_ptr 管理 → 泄漏
这是因为 new T(...) 表达式本身在 shared_ptr 构造函数体执行前就完成了对象构造——异常发生在构造期间,shared_ptr 构造函数甚至没机会接管原始指针。
-
make_shared内部用::operator new分配足够空间后,在该内存上用placement new构造对象,整个过程被封装在函数内部,异常路径已处理 - 注意:如果
T的构造函数抛异常,make_shared会自动释放之前分配的整块内存(含控制块空间)
哪些情况不能用 make_shared
不是所有场景都能替换。以下情形必须绕过 make_shared:
- 需要自定义删除器(例如文件句柄、C API 资源):
std::shared_ptr<file>(fopen(...), fclose)</file>无法用make_shared - 类禁用了
operator new(且未提供匹配的 placement new) - 需要将同一个对象同时交给多个
shared_ptr管理(即 aliasing constructor 场景),此时必须用原始指针构造 - 对象类型没有 public 构造函数,但 friend 类可访问其私有构造 ——
make_shared无法穿透访问权限(它在外部调用)
控制块与对象是否共分配,是编译器无关的语义保证,但具体内存对齐、头部大小属于实现细节。实际压测中,高频创建/销毁小对象时,make_shared 的吞吐量通常高出 10%–30%,主要来自分配器压力下降和 cache 友好性。真正要注意的,是它不解决循环引用问题,也不改变 shared_ptr 本身的线程安全边界。










