C++make_shared高效创建共享对象

2026-04-11 07:25:27 1271阅读 0评论

make_shared 不只是语法糖:它真能省下一次内存分配

上周帮同事调一个内存泄漏的 bug,最后发现根源不在逻辑错误,而在一行看似无害的代码:

auto ptr = std::shared_ptr<Foo>(new Foo(42, "hello"));

他以为这只是“写法不同”,但 valgrind 显示:每次构造都多分配了 16 字节——那正是 shared_ptr 控制块(control block)的额外开销。而换成:

auto ptr = std::make_shared<Foo>(42, "hello");

内存分配次数直接从 2 次降为 1 次

这不只是教科书里的性能彩蛋,而是 C++11 以来被低估的底层优化实招。


shared_ptr 的设计很聪明:它把引用计数、弱引用计数、删除器等元数据全塞进一个叫“控制块”的结构里。问题来了——这个控制块存在哪儿?
如果你用 shared_ptr<T>(new T)T 对象和控制块是分开分配的:一次 new T,一次 new control_block。两次堆分配,两次 CPU 缓存未命中,还可能跨页。

make_shared 干了一件很实在的事:一次性申请足够大的连续内存,前段放对象,后段放控制块。就像你去超市买菜和保鲜袋——拎两个袋子不如一个带分隔层的收纳盒利落。

这不是理论推演。在高频创建场景下(比如网络服务中每秒生成数千个请求上下文),减少一次 malloc 调用,意味着更少的锁竞争、更低的 TLB 压力、更可预测的延迟。我们线上一个日志聚合模块,把 shared_ptr<LogEntry> 的构造全切到 make_shared 后,P99 分配耗时下降了 37%。


当然,它不是万能钥匙。有三类情况你得收住手:

  • 自定义删除器make_shared 只支持默认删除器(即 delete)。一旦你写了 shared_ptr<Foo>(new Foo, [](Foo* p){ custom_cleanup(p); }),就只能手动构造。
  • 需要 enable_shared_from_this 的类make_shared 会提前构造对象,此时 weak_ptr 还没初始化,shared_from_this() 在构造函数里调用会抛 bad_weak_ptr。这类对象得老老实实用 shared_ptr 构造。
  • 对象构造可能抛异常,且你依赖控制块生命周期精确可控:虽然 make_shared 是强异常安全的(要么全成功,要么全不分配),但它的“原子性”反而让某些调试场景难追踪——比如你想观察控制块单独析构的行为。

这些限制不是缺陷,而是设计取舍:make_shared 优先保障常见路径的极致效率,而非覆盖所有边缘用例。


还有一个容易被忽略的细节:参数转发的精准性

make_shared 内部用的是完美转发(std::forward<Args>(args)...),这意味着它传递给 T 构造函数的,就是你写的原始参数。
对比一下:

struct Widget {
    Widget(std::string s) { /* s 是拷贝构造 */ }
};

// 以下两行行为一致:
auto a = std::make_shared<Widget>("hello");     // const char[6] → std::string(移动构造)
auto b = std::shared_ptr<Widget>(new Widget("hello")); // 同样走移动构造

但如果你传的是左值:

std::string msg = "hello";
auto c = std::make_shared<Widget>(msg);        // msg 被拷贝(非移动!)
auto d = std::shared_ptr<Widget>(new Widget(msg)); // 同样拷贝

所以别误以为 make_shared 会自动帮你“优化掉拷贝”——它只负责把你的参数原样送进去,怎么构造,还是 Widget 自己说了算。


实际项目中,我们定了一条小规矩:只要构造参数不涉及自定义删除器或 enable_shared_from_this,一律用 make_shared。CI 流水线里甚至加了 clang-tidy 检查项,对 shared_ptr<T>(new T(...)) 发出警告。

刚开始有人嫌麻烦:“就少一次分配,值得大动干戈?”
后来压测发现,当对象尺寸小(如 shared_ptr<int>)、但创建频率极高时,make_shared 的优势反而更明显——因为控制块本身大小固定,小对象的相对开销占比更高。

再往后,团队慢慢养成了肌肉记忆:看到 newshared_ptr 同框,手指就会自动改写。


make_shared 不是炫技的语法糖,它是 C++ 在抽象与效率之间亲手拧紧的一颗螺丝。它不声张,但每次分配都在默默省下一次系统调用;它不复杂,却要求你理解 shared_ptr 底层如何安放自己。

下次当你敲下 shared_ptr<T>(new T(...)),不妨停半秒:那个控制块,真的需要独自流浪在堆上吗?

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,1271人围观)

还没有评论,来说两句吧...

目录[+]