文章目录

C++ std::shared_ptr的make_shared与构造函数创建的性能对比

发布于 2026-05-04 09:16:15 · 浏览 22 次 · 评论 0 条

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());

此时发生了以下内存操作:

  1. 执行 new Widget():系统在堆上分配一块内存用于存放 Widget 对象本身。
  2. 执行 shared_ptr 构造函数:系统需要在堆上再次分配一块内存用于存放“控制块”。
  3. 组合:构造函数将对象指针与控制块关联起来。

这种方式会导致两次堆内存分配操作。

2. std::make_shared 方式

当使用 std::make_shared 创建智能指针时,内存分配过程被优化为一步。

编写如下代码观察行为:

auto p2 = std::make_shared<Widget>();

此时发生的内存操作:

  1. 计算系统所需的总内存大小(即 Widget 对象大小 + 控制块大小)。
  2. 执行单次内存分配:系统在堆上分配一块连续的内存空间,同时容纳对象和控制块。
  3. 构造:在同一块内存上调用 Widget 的构造函数并初始化控制块。

这种方式仅需要进行一次堆内存分配。


可视化内存布局

为了更直观地展示两者在内存布局上的区别,请参考下方的结构图。

graph TD subgraph A ["构造函数创建 (new + shared_ptr)"] direction TB A1["内存块 1: Widget 对象\n(由 new 分配)"] A2["内存块 2: 控制块\n(由 shared_ptr 构造分配)"] end subgraph B ["make_shared 创建"] direction TB B1["单一连续内存块:\n1. Widget 对象\n2. 控制块"] end style A1 fill:#f9f,stroke:#333,stroke-width:2px style A2 fill:#bbf,stroke:#333,stroke-width:2px style B1 fill:#bfb,stroke:#333,stroke-width:2px

从图中可以看出,make_shared 将数据整合在一起,减少了内存碎片,并提升了分配效率。


性能对比分析

基于上述的内存分配机制,我们可以从以下三个维度对两者进行深入对比。

对比维度 构造函数创建 (new) std::make_shared 差异解析
内存分配次数 2次 1次 make_shared 将对象与控制块合并分配,减少了 operator new 的调用开销,显著提升性能。
内存开销 分离的内存块 连续的内存块 make_shared 减少了内存管理的元数据开销,且局部性更好,能提升缓存命中率。
异常安全性 如果在 new 之后、shared_ptr 构造之前发生异常(例如函数调用参数求值顺序问题),内存可能泄露。make_shared 能够完全避免此风险。

权衡与隐藏的陷阱

虽然 make_shared 在性能和安全性上通常更优,但在特定场景下,它也有一个明显的缺点:控制块的生命周期。

理解引用计数的销毁时机:

  1. 构造函数方式:当对象的引用计数降为 0,Widget 的内存被立即释放。此时即使控制块(因为弱引用 std::weak_ptr 存在)仍然存活,对象占用的内存也已经归还给系统。
  2. make_shared方式:由于对象和控制块在同一块内存中,只要控制块还活着(例如还有 weak_ptr 指向它),整块内存都无法被释放。

这意昧着,如果对象非常大(例如一个 100MB 的图像数据),且存在 std::weak_ptr 长期持有该对象不释放,使用 make_shared 会导致这 100MB 的内存长期被占用,即使该对象已经不再被使用。而使用 new 方式,那 100MB 可以被先释放,仅保留微小的控制块内存。


实操指南与最佳实践

根据上述分析,遵循以下步骤选择合适的创建方式:

  1. 优先使用 std::make_shared
    在绝大多数常规场景下(对象大小适中,无需配合 std::weak_ptr 进行复杂生命周期管理),直接使用 std::make_shared。它能带来更好的性能和更强的异常安全保证。

    // 推荐:默认选择
    auto ptr = std::make_shared<MyClass>("arguments");
  2. 使用构造函数方式 当且仅当:

    • 自定义删除器:你需要为对象指定特定的删除逻辑。
    • 大对象 + 弱引用:对象占用内存很大,且会被 std::weak_ptr 长期监控,为了避免内存占用延迟释放。
    // 特殊情况:大对象配合 weak_ptr
    std::shared_ptr<BigObject> ptr(new BigObject());
  3. 避免混用
    绝不要将 new 得来的原始指针赋值给多个 shared_ptr,这会导致重复释放和崩溃。

    // 错误示范
    auto raw = new int();
    std::shared_ptr<int> p1(raw);
    std::shared_ptr<int> p2(raw); // 危险!p1 和 p2 各自创建独立的控制块,导致 raw 被 delete 两次

通过理解内存分配的底层逻辑,可以在保证代码健壮性的同时,榨取程序的最后一点性能。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文