找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1163

积分

0

好友

163

主题
发表于 3 天前 | 查看: 9| 回复: 0

图片

看到这个问题,不禁让人想起一些真实的生产环境教训。例如,在一次线上问题的排查中,最终定位到一个偶发的内存泄漏,其根源正是由于不当使用 shared_ptr(new T()) 导致的构造异常安全问题。如果当时采用了 make_shared,这个隐患完全可以避免。

简而言之,std::make_shared 相比直接 new 有以下核心优势:性能更好、更安全、代码更简洁。理解其背后的原理和适用边界,是编写健壮、高效C++代码的基本功。

一、核心区别:一次分配 vs 两次分配

最关键的区别在于内存分配的机制。

使用 new 的方式:

std::shared_ptr<Widget> sp(new Widget());

这种方式至少需要两次独立的内存分配:

  1. Widget 对象本身分配内存。
  2. shared_ptr 的控制块(存储引用计数、弱引用计数、删除器等)分配内存。

其内存布局大致如下:

[Widget对象]  <--- 第一次分配
    ...
[控制块(引用计数等)]  <--- 第二次分配

使用 make_shared 的方式:

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

std::make_shared 通常通过一次内存分配,在连续的内存空间中同时容纳控制块和对象本身。

[控制块 + Widget对象]  <--- 一次分配搞定

性能提升体现在哪里?

  • 减少分配开销:减少一次内存分配/释放的系统调用。
  • 提升缓存局部性:对象和控制块位置相邻,访问时CPU缓存命中率更高。
  • 减少内存碎片:更少、更大的内存块分配有助于降低内存碎片。

在需要频繁创建和销毁shared_ptr高性能场景或并发编程中,这种优化带来的累积效果非常可观。正确使用智能指针是构建健壮并发程序的重要基础。

二、异常安全性:这个更致命

这是一个容易被忽视但后果严重的陷阱。

考虑以下函数和调用:

void processWidget(std::shared_ptr<Widget> sp, int priority);

// 调用方式1: 使用 new (存在风险!)
processWidget(std::shared_ptr<Widget>(new Widget()), computePriority());

// 调用方式2: 使用 make_shared (安全)
processWidget(std::make_shared<Widget>(), computePriority());

第一种方式的风险在哪里?

C++标准并未规定函数参数的求值顺序。对于 processWidget(new Widget(), computePriority()),编译器可能的执行顺序之一是:

  1. new Widget() —— 成功分配内存。
  2. computePriority() —— 执行时抛出异常
  3. std::shared_ptr<Widget>(...) —— 由于异常发生,此构造步骤永远不会执行

结果就是:Widget 对象的内存已经被分配,但负责管理它的 shared_ptr 却未被成功构造,导致内存泄漏

std::make_shared 是一个单一的函数调用,它会在内部完成内存分配和智能指针的构造,从而保证了强异常安全性,computePriority 的异常不会导致内存泄漏。

三、代码简洁性

make_shared 在语法上也更具优势。

// 繁琐且需要重复书写长类型名
std::shared_ptr<SomeVeryLongTypeName> sp1(new SomeVeryLongTypeName(arg1, arg2, arg3));

// 简洁优雅,借助auto关键字
auto sp2 = std::make_shared<SomeVeryLongTypeName>(arg1, arg2, arg3);

无需重复书写类型,代码更简洁,可读性和维护性都更好。

四、make_shared 的局限性

当然,make_shared 并非银弹,在以下特定场景中,仍需使用 new 配合 shared_ptr 构造函数。

1. 需要自定义删除器

make_shared 无法指定自定义删除器。

// make_shared 不支持此方式,必须使用 new
auto sp = std::shared_ptr<FILE>(fopen("file.txt", "r"), fclose);

2. 构造函数是私有的或受保护的

make_shared 作为非成员函数,无法访问类的私有或保护构造函数。

class Singleton {
private:
    Singleton() {}
public:
    static std::shared_ptr<Singleton> create() {
        // 此处无法使用 make_shared
        return std::shared_ptr<Singleton>(new Singleton());
    }
};

3. 对象内存延迟释放问题(与 weak_ptr 相关)

当使用 make_shared 时,对象和控制块位于同一内存块。如果有 std::weak_ptr 长期存在(即使对应的 shared_ptr 已销毁),由于控制块仍需存活以跟踪弱引用计数,整个内存块(包括对象所占用的部分)都无法被释放

auto sp = std::make_shared<HugeObject>(); // 假设对象体积非常大
std::weak_ptr<HugeObject> wp = sp;
sp.reset(); // shared_ptr 引用计数归零,对象析构函数被调用
// 但是,只要 wp 还存在,为 HugeObject 分配的那部分内存就无法归还给系统

如果采用 new 的方式,对象和控制块分离,对象被销毁后,其内存可以立即释放,控制块则会等到所有 weak_ptr 释放后再回收。

五、最佳实践总结

默认情况下,应优先使用 make_shared

auto sp = std::make_shared<T>(args...);

仅在以下特定情况考虑使用 new

  1. 需要指定自定义删除器。
  2. 需要构造一个构造函数为非公有的类对象。
  3. 对象体积巨大,且预期会有长生命周期的 weak_ptr 引用,需要避免内存延迟释放。
  4. 在 C++20 之前,需要使用大括号初始化列表构造对象。

六、补充:make_unique 的类似建议

对于 std::unique_ptr,C++14 引入了 std::make_unique,其建议同理:

// 推荐:异常安全且简洁
auto up = std::make_unique<T>(args...);

// 不推荐
std::unique_ptr<T> up(new T(args...));

它同样具备异常安全性和代码简洁性的优势,是现代C++编程中的首选方式。掌握这些智能指针的最佳实践,是深入理解C++内存管理模型与系统编程的关键一步。




上一篇:WPF边缘网关开发实战:低代码配置PLC数据采集与HTTP上报方案
下一篇:Worth-Calculator工作性价比计算器:数据驱动的职场决策与薪资评估工具
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 16:02 , Processed in 0.118177 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表