std::make_shared能避免两次内存分配,因为它将对象和控制块分配在同一块连续内存中,仅调用一次operator new,这是c++11标准强制要求的单次分配行为。

std::make_shared 为什么能避免两次内存分配
因为 std::shared_ptr 内部需要两块内存:一块存对象本身,另一块存控制块(引用计数、弱引用计数等)。直接用 new 构造再传给 std::shared_ptr 构造函数时,编译器得分别申请这两块内存——一次给对象,一次给控制块。而 std::make_shared 把它们“打包”进同一块连续内存里,只调一次 operator new。
这不是理论优化,是标准强制要求的行为:C++11 起,std::make_shared 必须以单次分配实现对象+控制块的布局。
哪些场景下 std::make_shared 会失效或倒退
它只适用于构造函数可直接调用的场景;一旦绕不开显式 new 或需要自定义删除器、别名构造、或对象类型不完整(前向声明),就只能退回到裸指针构造 std::shared_ptr。
- 类没有 public 构造函数(比如只有
friend工厂函数)→ 无法在make_shared内部调用构造 - 需要绑定自定义删除器(如
std::shared_ptr<foo>(new Foo, [](Foo* p){...})</foo>)→make_shared不接受删除器参数 - 想构造派生类指针但用基类
shared_ptr接收(即别名构造)→make_shared返回的是精确类型的shared_ptr,不能直接 alias - 类内有
operator new重载且依赖特定分配逻辑 →make_shared会绕过它,改用全局或类模板分配器
std::make_shared 的参数转发陷阱
它完美转发参数给目标类型的构造函数,但容易忽略隐式转换和临时对象生命周期问题。最典型的是把 const 引用成员初始化搞错。
立即学习“C++免费学习笔记(深入)”;
例如:
struct Box { const std::string& s; Box(const std::string& s_) : s(s_) {} };
auto p = std::make_shared<Box>("hello"); // 错!"hello" 是临时 string,绑定到引用后在构造结束就销毁这类错误编译不报,但运行时 p->s 是悬垂引用。根本原因是字符串字面量先隐式转成临时 std::string,再绑定给引用成员——而这个临时对象只活到构造函数结束。
- 避免对 const 引用成员做隐式转换初始化
- 优先用值语义(
std::string s;)或移动语义(std::string&& s_) - 不确定时,宁可用
std::shared_ptr<t>(new T{...})</t>显式控制临时对象生命周期
性能差异在什么规模下才值得计较
单次分配省下的只是几十纳秒,但累积效应明显:高频创建/销毁 shared_ptr(如网络请求上下文、事件循环中的小对象)时,分配次数减半 + 缓存局部性提升,实测 heap 分配压力可降 15%~30%。
不过要注意:如果对象本身很大(比如几 MB),那控制块那几十字节的节省就毫无意义;反而可能因内存对齐导致实际分配更多字节(make_shared 需同时满足对象和控制块的对齐要求)。
- 小对象(10k/s)创建 → 用
make_shared几乎总是更好 - 大对象或低频使用 → 分配次数差异微不足道,优先考虑语义清晰度和兼容性
- 调试构建下,某些 STL 实现(如 libc++)会禁用
make_shared的优化以方便内存检查,所以压测务必用 Release 模式
真正容易被忽略的是:即使你写了 make_shared,如果目标类型构造函数抛异常,控制块和对象内存仍会被自动释放——这是安全保证,但也是唯一例外:它内部仍可能触发两次 deallocation(先析构对象,再释放整块内存)。不过这不影响“分配只一次”的承诺。











