C++make_shared高效创建共享对象
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 的优势反而更明显——因为控制块本身大小固定,小对象的相对开销占比更高。
再往后,团队慢慢养成了肌肉记忆:看到 new 和 shared_ptr 同框,手指就会自动改写。
make_shared 不是炫技的语法糖,它是 C++ 在抽象与效率之间亲手拧紧的一颗螺丝。它不声张,但每次分配都在默默省下一次系统调用;它不复杂,却要求你理解 shared_ptr 底层如何安放自己。
下次当你敲下 shared_ptr<T>(new T(...)),不妨停半秒:那个控制块,真的需要独自流浪在堆上吗?


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