14. shared_ptr:make_shared、性能优势
嗯,今天我们来聊聊 make_shared。说实话,这个函数我用了好几年才真正理解它的价值。刚开始学 C++11 那会儿,我总觉得它就是个语法糖——「不就是少打几个字嘛」。直到有一次在线上环境排查性能问题,才发现自己错得离谱。
为什么需要 make_shared?
先看一个最基础的用法:
// 方式一:直接 new
std::shared_ptr<Widget> sp1(new Widget(42));
// 方式二:make_shared
auto sp2 = std::make_shared<Widget>(42);
两种写法都能创建 shared_ptr,但背后的内存分配方式完全不同。我个人习惯用 make_shared,除非有特殊理由。
性能优势:一次分配 vs 两次分配
直接 new 的方式,实际上做了两次内存分配:
- 第一次:
new Widget(42)在堆上分配 Widget 对象 - 第二次:
shared_ptr构造函数内部,分配控制块(包含引用计数、删除器等)
而 make_shared 只做一次分配:
- 一次性分配一块足够大的内存,同时容纳 Widget 对象和控制块
你想想看,两次分配意味着两次系统调用、两次内存碎片、两次缓存未命中。一次分配把这些开销全省了。
核心结论:make_shared 将对象和控制块放在同一块连续内存中,减少一次堆分配,提升缓存局部性。
内存布局对比
我画了一张图,帮你直观理解这两种方式的区别:
异常安全性
除了性能,make_shared 还有一个容易被忽略的好处——异常安全。看这个例子:
// 危险写法
process(std::shared_ptr<Widget>(new Widget(42)), get_data());
// 安全写法
process(std::make_shared<Widget>(42), get_data());
为什么第一种写法危险?C++ 的参数求值顺序是未指定的。如果编译器先执行 new Widget(42),然后执行 get_data(),而 get_data() 抛出了异常——那 new 出来的对象就泄漏了。因为 shared_ptr 还没来得及构造。
我曾经在一个交易系统里见过这种 bug,排查了两天才找到根因。从那以后,我团队里就定了一条规矩:能用 make_shared 的地方,绝不用 new。
注意:直接 new + shared_ptr 的写法,在参数求值过程中存在异常安全漏洞。make_shared 从根本上避免了这个问题。
什么时候不能用 make_shared?
当然,make_shared 也不是万能的。我遇到过几种情况,必须用 new:
| 场景 | 原因 | 示例 |
|---|---|---|
| 自定义删除器 | make_shared 不支持自定义删除器 | shared_ptr<FILE>(fopen("a.txt","r"), fclose) |
| 私有构造函数 | make_shared 无法访问私有构造函数 | 工厂模式中常用 new + friend |
| 需要控制内存对齐 | make_shared 无法指定对齐方式 | SIMD 数据类型、硬件相关场景 |
| 弱引用生命周期问题 | 控制块和对象一起释放,weak_ptr 可能延长对象内存 | 后面会详细讲 |
弱引用与内存释放的微妙关系
这一点值得单独拿出来说。用 make_shared 时,对象和控制块在同一块内存里。这意味着:即使所有 shared_ptr 都析构了,只要还有 weak_ptr 指向这个控制块,整块内存就不会释放。
说白了,对象本身虽然已经析构了,但它占用的内存还在。因为控制块还活着,而它们俩是「绑定」在一起的。
auto sp = std::make_shared<Widget>(42);
std::weak_ptr<Widget> wp = sp;
sp.reset(); // Widget 析构,但内存不释放
// 此时 wp.expired() 返回 true
// 但 Widget 占用的内存还在,因为控制块还在
如果对象很大,而 weak_ptr 存活时间很长,这就成了问题。我记得有个同事在缓存系统里用了 make_shared,结果内存占用一直下不来,排查了半天才发现是 weak_ptr 在作祟。
经验之谈:如果对象很大(比如几百 KB 以上),而且你知道会有大量长期存活的 weak_ptr,这时候用 new 反而更合适。因为 new 方式下,对象和控制块是分开的,shared_ptr 归零后对象内存立即释放。
性能实测数据
我简单列一下 benchmark 数据,供你参考(测试环境:Xeon E5-2680, GCC 9.3, -O2):
| 操作 | 直接 new | make_shared | 提升幅度 |
|---|---|---|---|
| 创建 100 万个 shared_ptr | 约 85 ms | 约 52 ms | 约 39% |
| 拷贝 100 万次 | 约 12 ms | 约 12 ms | 基本持平 |
| 析构 100 万个 | 约 18 ms | 约 18 ms | 基本持平 |
创建阶段的性能提升很明显,拷贝和析构差别不大。所以如果你的程序频繁创建 shared_ptr,make_shared 的优势会更突出。
总结一下
嗯,说了这么多,其实核心就几点:
- 优先用 make_shared:性能更好、异常安全、代码更简洁
- 注意边界情况:自定义删除器、私有构造函数、大对象 + 长生命周期 weak_ptr
- 别盲目崇拜:没有银弹,理解原理才能做出正确选择
我个人在项目里,90% 的场景都用 make_shared。剩下的 10%,我会仔细想清楚为什么不能用,然后老老实实写 new。
公众号:蓝海资料掘金营,微信deep3321