C++ std::shared_ptr的make_shared与构造函数创建的性能对比
在C++开发中,使用智能指针是管理动态内存的标准做法。然而,创建 std::shared_ptr 主要有两种方式:直接使用 new 配合构造函数,或者使用 std::make_shared 工厂函数。这两种方式在性能表现和内存布局上存在显著差异,直接影响了程序的运行效率。
理解内存分配机制
要理解性能差异,首先需要明白 std::shared_ptr 在内存中到底有什么。它不仅包含指向对象的指针,还必须包含一个“控制块”,用于存储引用计数、弱引用计数和删除器。
1. 传统构造函数方式(使用 new)
当通过 new 创建对象并传递给 shared_ptr 构造函数时,内存分配过程分为两步。
编写如下代码观察行为:
std::shared_ptr<Widget> p1(new Widget());
此时发生了以下内存操作:
- 执行
new Widget():系统在堆上分配一块内存用于存放Widget对象本身。 - 执行
shared_ptr构造函数:系统需要在堆上再次分配一块内存用于存放“控制块”。 - 组合:构造函数将对象指针与控制块关联起来。
这种方式会导致两次堆内存分配操作。
2. std::make_shared 方式
当使用 std::make_shared 创建智能指针时,内存分配过程被优化为一步。
编写如下代码观察行为:
auto p2 = std::make_shared<Widget>();
此时发生的内存操作:
- 计算系统所需的总内存大小(即
Widget对象大小 + 控制块大小)。 - 执行单次内存分配:系统在堆上分配一块连续的内存空间,同时容纳对象和控制块。
- 构造:在同一块内存上调用
Widget的构造函数并初始化控制块。
这种方式仅需要进行一次堆内存分配。
可视化内存布局
为了更直观地展示两者在内存布局上的区别,请参考下方的结构图。
从图中可以看出,make_shared 将数据整合在一起,减少了内存碎片,并提升了分配效率。
性能对比分析
基于上述的内存分配机制,我们可以从以下三个维度对两者进行深入对比。
| 对比维度 | 构造函数创建 (new) |
std::make_shared |
差异解析 |
|---|---|---|---|
| 内存分配次数 | 2次 | 1次 | make_shared 将对象与控制块合并分配,减少了 operator new 的调用开销,显著提升性能。 |
| 内存开销 | 分离的内存块 | 连续的内存块 | make_shared 减少了内存管理的元数据开销,且局部性更好,能提升缓存命中率。 |
| 异常安全性 | 弱 | 强 | 如果在 new 之后、shared_ptr 构造之前发生异常(例如函数调用参数求值顺序问题),内存可能泄露。make_shared 能够完全避免此风险。 |
权衡与隐藏的陷阱
虽然 make_shared 在性能和安全性上通常更优,但在特定场景下,它也有一个明显的缺点:控制块的生命周期。
理解引用计数的销毁时机:
- 构造函数方式:当对象的引用计数降为 0,
Widget的内存被立即释放。此时即使控制块(因为弱引用std::weak_ptr存在)仍然存活,对象占用的内存也已经归还给系统。 - make_shared方式:由于对象和控制块在同一块内存中,只要控制块还活着(例如还有
weak_ptr指向它),整块内存都无法被释放。
这意昧着,如果对象非常大(例如一个 100MB 的图像数据),且存在 std::weak_ptr 长期持有该对象不释放,使用 make_shared 会导致这 100MB 的内存长期被占用,即使该对象已经不再被使用。而使用 new 方式,那 100MB 可以被先释放,仅保留微小的控制块内存。
实操指南与最佳实践
根据上述分析,遵循以下步骤选择合适的创建方式:
-
优先使用
std::make_shared:
在绝大多数常规场景下(对象大小适中,无需配合std::weak_ptr进行复杂生命周期管理),直接使用std::make_shared。它能带来更好的性能和更强的异常安全保证。// 推荐:默认选择 auto ptr = std::make_shared<MyClass>("arguments"); -
使用构造函数方式 当且仅当:
- 自定义删除器:你需要为对象指定特定的删除逻辑。
- 大对象 + 弱引用:对象占用内存很大,且会被
std::weak_ptr长期监控,为了避免内存占用延迟释放。
// 特殊情况:大对象配合 weak_ptr std::shared_ptr<BigObject> ptr(new BigObject()); -
避免混用:
绝不要将new得来的原始指针赋值给多个shared_ptr,这会导致重复释放和崩溃。// 错误示范 auto raw = new int(); std::shared_ptr<int> p1(raw); std::shared_ptr<int> p2(raw); // 危险!p1 和 p2 各自创建独立的控制块,导致 raw 被 delete 两次
通过理解内存分配的底层逻辑,可以在保证代码健壮性的同时,榨取程序的最后一点性能。

暂无评论,快来抢沙发吧!