当前位置:

首页 > 编程开发 > C++中std::shared_ptr使用详解

C++中std::shared_ptr使用详解

std::shared_ptr通过引用计数实现共享所有权的自动内存管理,避免内存泄漏和悬空指针;推荐使用std::make_shared创建,注意循环引用等陷阱。

本文目录

    std::shared_ptr通过引用计数实现共享所有权的自动内存管理,避免内存泄漏和悬空指针;推荐使用std::make_shared创建,注意循环引用等陷阱。

    如何在C++中使用std::shared_ptr_C++共享指针shared_ptr使用指南

    std::shared_ptr在C++中提供了一种智能、自动管理动态内存的方式,它允许我们以共享所有权(shared ownership)的模式来管理堆上的对象。简单来说,它就像一个智能的引用计数器,当指向同一对象的最后一个shared_ptr被销毁时,它会自动释放所管理的对象内存,有效避免了内存泄漏和悬空指针的问题。

    解决方案

    使用std::shared_ptr,最直接的便是创建并让它管理一个对象。通常,我们推荐使用std::make_shared来创建shared_ptr,这不仅效率更高(单次内存分配),也能避免一些潜在的内存泄漏问题。

    #include 
    #include  // 包含 shared_ptr
    
    class MyClass {
    public:
        MyClass() { std::cout << "MyClass 构造\n"; }
        ~MyClass() { std::cout << "MyClass 析构\n"; }
        void doSomething() { std::cout << "MyClass 正在工作...\n"; }
    };
    
    int main() {
        // 推荐方式:使用 std::make_shared
        std::shared_ptr ptr1 = std::make_shared();
        ptr1->doSomething();
        std::cout << "ptr1 的引用计数: " << ptr1.use_count() << std::endl; // 通常是1
    
        // 复制 shared_ptr,共享所有权
        std::shared_ptr ptr2 = ptr1;
        std::cout << "ptr1 的引用计数: " << ptr1.use_count() << std::endl; // 此时是2
        std::cout << "ptr2 的引用计数: " << ptr2.use_count() << std::endl; // 此时是2
    
        // 另一个 shared_ptr 也指向同一个对象
        {
            std::shared_ptr ptr3(ptr1); // 或 std::shared_ptr ptr3 = ptr1;
            std::cout << "ptr1 的引用计数: " << ptr1.use_count() << std::endl; // 此时是3
        } // ptr3 在这里离开作用域,引用计数减1
    
        std::cout << "ptr1 的引用计数 (ptr3 离开作用域后): " << ptr1.use_count() << std::endl; // 此时是2
    
        // 可以通过 get() 获取原始指针,但要小心使用,不要手动删除
        MyClass* rawPtr = ptr1.get();
        if (rawPtr) {
            rawPtr->doSomething();
        }
    
        // 重置 shared_ptr,使其不再管理当前对象
        ptr1.reset(); // ptr1 现在为空,引用计数减1
        std::cout << "ptr1 重置后,ptr2 的引用计数: " << ptr2.use_count() << std::endl; // 此时是1
    
        // 当最后一个 shared_ptr (ptr2) 离开作用域时,MyClass 对象将被析构
        return 0;
    }

    这段代码展示了shared_ptr的核心机制:创建、复制、引用计数的变化以及最终的自动释放。通过->和*运算符可以像使用普通指针一样访问其管理的对象。use_count()方法则可以查看当前有多少个shared_ptr实例共享同一个对象。

    std::shared_ptr与普通指针有何不同?为何要选择它?

    说实话,刚接触C++时,指针这东西确实让人又爱又恨。它强大,能直接操作内存,但同时也带来了无数的陷阱:内存泄漏、野指针、重复释放等等。std::shared_ptr的出现,在我看来,就是为了解决这些“人祸”而生。它和普通指针最大的不同,在于它拥有所有权,并且这种所有权是共享的。

    普通指针,说白了,就是个地址,它不关心它指向的内存是谁分配的,谁应该释放。你手动new一个对象,就得手动delete它,否则就是泄漏。如果你不小心delete了两次,程序就崩溃了。更别提多个指针指向同一块内存,你不知道什么时候这块内存被其他指针释放了,你的指针就成了“悬空指针”,访问它就是未定义行为。

    而std::shared_ptr则不然,它内部有一个引用计数器。当你创建一个shared_ptr并让它指向一个对象时,引用计数器就初始化为1。每当你复制这个shared_ptr(比如std::shared_ptr p2 = p1;),引用计数器就加1。当一个shared_ptr离开作用域或被重置时,引用计数器就减1。只有当引用计数器归零时,也就是没有shared_ptr再指向这个对象时,它才会自动调用对象的析构函数并释放内存。这简直是“懒人福音”,把内存管理的重担从程序员肩上卸了下来。

    选择std::shared_ptr的理由很直接:

    1. 自动内存管理:这是核心优势。遵循RAII(Resource Acquisition Is Initialization)原则,资源(内存)的生命周期与对象的生命周期绑定,无需手动释放,大大降低了内存泄漏的风险。
    2. 共享所有权:在多处需要访问并共享同一对象,且对象生命周期不确定时,shared_ptr是理想选择。比如一个资源被多个模块使用,只要有一个模块还在用,资源就不会被释放。
    3. 避免悬空指针:当一个shared_ptr管理的对象被释放后,所有指向它的shared_ptr都会变成空(虽然它们本身并不会自动感知,但use_count为0后,访问行为会更可控,至少不会访问到已释放的内存)。更重要的是,它保证了对象在还有“主人”时不会被提前释放。
    // 示例:普通指针可能带来的问题
    void process_raw_ptr(int* data) {
        if (data) {
            // ... 使用 data
            delete data; // 假设这里要释放
        }
    }
    
    int main_raw() {
        int* my_data = new int(100);
        process_raw_ptr(my_data);
        // 此时 my_data 已经是一个悬空指针,再次访问或 delete 会出问题
        // std::cout << *my_data << std::endl; // 未定义行为
        // delete my_data; // 双重释放
    
        // 使用 shared_ptr 则不会有这些烦恼
        std::shared_ptr shared_data = std::make_shared(100);
        std::shared_ptr shared_data_copy = shared_data;
        // 无论哪个 shared_ptr 离开作用域,只要还有其他 shared_ptr 引用,内存就不会被释放
        // 只有当 shared_data 和 shared_data_copy 都失效后,int(100) 才会析构
        return 0;
    }

    这种由shared_ptr提供的“智能”和“安全”,在现代C++编程中是极其宝贵的。

    std::shared_ptr的常见陷阱与最佳实践是什么?

    虽然shared_ptr极大地简化了内存管理,但它也不是万能的,甚至可以说,如果用得不好,它也会引入一些新的复杂性。

    常见陷阱:

    1. 循环引用 (Circular References):这是shared_ptr最臭名昭著的陷阱。当两个或多个对象相互持有对方的shared_ptr时,它们会形成一个引用环。即使外部没有其他shared_ptr指向它们,它们的引用计数也永远不会降到零,导致这组对象永远不会被释放,造成内存泄漏。

      class B; // 前向声明
      class A {
      public:
          std::shared_ptr b_ptr;
          ~A() { std::cout << "A 析构\n"; }
      };
      
      class B {
      public:
          std::shared_ptr a_ptr;
          ~B() { std::cout << "B 析构\n"; }
      };
      
      void test_circular_ref() {
          std::shared_ptr a = std::make_shared();
          std::shared_ptr b = std::make_shared();
          a->b_ptr = b; // b 的引用计数变为 2
          b->a_ptr = a; // a 的引用计数变为 2
          // 当 a 和 b 离开作用域时,它们的引用计数都还是 1,导致 A 和 B 都不会被析构
      } // A 和 B 都没有析构,内存泄漏!

      解决循环引用通常需要引入std::weak_ptr。weak_ptr是一种不增加引用计数的智能指针,它“观察”shared_ptr管理的对象,但不拥有它。

    2. 从裸指针多次构造shared_ptr:如果你有一个裸指针,然后用它创建了多个独立的shared_ptr,而不是通过复制一个已存在的shared_ptr,那么每个shared_ptr都会有自己的引用计数器,并认为自己是该对象的唯一所有者。当它们各自的引用计数归零时,就会尝试多次释放同一块内存,导致双重释放错误。

      int* raw_ptr = new int(42);
      std::shared_ptr p1(raw_ptr); // p1 认为自己是所有者
      // std::shared_ptr p2(raw_ptr); // 错误!p2 也会认为自己是所有者,导致双重释放
      // 正确的做法是:
      // std::shared_ptr p2 = p1;
    3. 从this指针创建shared_ptr:在类的成员函数中,如果你需要返回当前对象的shared_ptr,直接使用std::shared_ptr(this)也是错误的,因为它同样会创建一个独立的shared_ptr,导致与外部已有的shared_ptr冲突。正确的方法是让类继承std::enable_shared_from_this,并通过shared_from_this()方法获取。

    4. 管理非堆内存或数组:shared_ptr默认调用delete来释放内存。如果它管理的是一个数组(new T[N]),则需要提供一个自定义的deleter来调用delete[]。对于非堆内存(如栈上的对象),shared_ptr也会尝试delete,导致未定义行为。

    最佳实践:

    1. 优先使用std::make_shared:这是最重要的一条。std::make_shared在一个内存块中同时分配对象和其控制块(包含引用计数等信息),减少了一次内存分配,提高了效率,并且避免了在构造对象失败时,控制块仍然被分配的潜在内存泄漏。

      // 推荐
      std::shared_ptr p = std::make_shared();
      // 不推荐,虽然功能上没问题,但效率较低,且可能出现异常安全问题
      // std::shared_ptr p(new MyClass());
    2. 使用std::weak_ptr解决循环引用:当两个对象需要相互引用,但又不希望形成强所有权循环时,让其中一方持有weak_ptr。

      class B_fixed;
      class A_fixed {
      public:
          std::shared_ptr b_ptr;
          ~A_fixed() { std::cout << "A_fixed 析构\n"; }
      };
      
      class B_fixed {
      public:
          std::weak_ptr a_ptr; // 使用 weak_ptr
          ~B_fixed() { std::cout << "B_fixed 析构\n"; }
      };
      
      void test_no_circular_ref() {
          std::shared_ptr a = std::make_shared();
          std::shared_ptr b = std::make_shared();
          a->b_ptr = b;
          b->a_ptr = a; // 这里不会增加 a 的引用计数
      } // a 和 b 都会正常析构
    3. 理解所有权语义:shared_ptr意味着共享所有权。如果一个对象应该只有一个所有者,或者其生命周期由其创建者严格控制,那么std::unique_ptr可能是更好的选择。

    4. 自定义deleter:如果shared_ptr需要管理非标准方式分配的内存(如malloc、new[])或者需要执行特殊的清理操作,可以提供一个自定义的deleter。

      // 管理数组
      std::shared_ptr arr_ptr(new int[10], std::default_delete());
      // 或使用 lambda 作为 deleter
      std::shared_ptr file_ptr(fopen("test.txt", "w"), [](FILE* f) {
          if (f) { fclose(f); std::cout << "文件关闭\n"; }
      });
    5. 避免在shared_ptr管理的对象内部持有其自身的shared_ptr:这通常会导致循环引用或不必要的复杂性。如果需要,请使用std::enable_shared_from_this。

    std::shared_ptr在实际项目中的应用场景与性能考量

    std::shared_ptr在实际项目中有着非常广泛的应用,尤其是在需要对象生命周期管理复杂、多模块共享资源、或者难以明确单一所有者的场景。

    应用场景:

    1. 工厂模式 (Factory Pattern):当工厂函数创建对象并返回给调用者时,通常会返回一个shared_ptr。这样,工厂本身不需要关心对象的生命周期,调用者可以安全地使用,并在不再需要时自动释放。
      // 假设有一个接口 Product
      // std::shared_ptr createProduct(ProductType type);
    2. 缓存管理 (Cache Management):在缓存系统中,数据项可能会被多个消费者同时使用。shared_ptr可以确保只要有任何一个消费者还在使用某个数据项,该数据项就不会被从缓存中移除或销毁。当所有消费者都释放了对它的shared_ptr时,缓存系统可以安全地将其清除。
    3. 多线程环境下的资源共享:在多线程应用中,多个线程可能需要访问同一个数据结构或资源。shared_ptr可以管理这些资源的生命周期,确保资源在所有使用它的线程都结束后才被释放。当然,shared_ptr本身是线程安全的(引用计数的增减是原子操作),但它所管理的对象的数据访问仍需要额外的同步机制(如互斥锁)。
    4. 图形渲染与游戏引擎:纹理、模型、着色器等资源常常被多个游戏对象或渲染通道共享。shared_ptr在这里可以有效地管理这些资源的生命周期,避免重复加载和提前释放。
    5. 事件系统或观察者模式:当一个事件源需要通知多个观察者时,观察者通常会以shared_ptr的形式注册到事件源。这样,只要有观察者存在,事件源就可以安全地向它们发送通知。

    性能考量:

    shared_ptr的便利性并非没有代价,它相比于裸指针和unique_ptr,会引入一些性能开销:

    1. 内存开销:shared_ptr不仅仅是一个指针。它需要一个额外的“控制块”来存储引用计数(强引用和弱引用)、自定义deleter等信息。这个控制块通常与对象本身分开分配(除非使用make_shared),因此每个shared_ptr实例会占用更多的内存。
    2. 运行时开销:
      • 引用计数操作:每次shared_ptr的复制、赋值、销毁都会涉及到引用计数的原子增减操作。原子操作虽然比非原子操作慢,但在多线程环境下是必要的。
      • 构造/析构:shared_ptr的构造和析构比裸指针或unique_ptr更复杂,因为它需要管理控制块和引用计数。
      • make_shared的优势:如前所述,std::make_shared能够将对象和控制块分配在同一块内存上,这不仅减少了内存碎片,也提升了缓存局部性,从而在一定程度上缓解了内存开销和部分运行时开销。

    何时选择shared_ptr,何时选择unique_ptr?

    这是一个很实际的问题。我的经验是,如果一个对象只有一个明确的、排他的所有者,并且所有权可以转移,那么std::unique_ptr是首选。它零运行时开销,内存开销也最小(只比裸指针多一个指针大小)。它强调“独占所有权”,语义更清晰。

    只有当一个对象确实需要被多个部分共享,并且其生命周期难以由单一所有者决定时,才考虑使用std::shared_ptr。比如上面提到的缓存、多线程共享资源等场景。

    总结来说,shared_ptr是一个强大的工具,它在C++中提供了自动的内存管理和共享所有权语义。理解其工作原理、优缺点以及潜在陷阱,并结合实际需求选择合适的智能指针,是编写高效、健壮现代C++代码的关键。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发
    相关文章 更多
    GitHub六个AI开源项目能做什么?软件理解、语音与求职场景
    GitHub六个AI开源项目能做什么?软件理解、语音与求职场景

    解析六个GitHub AI开源项目的具体用途,包括软件静态分析、智能体回答检查、多语言配音、求职材料生成、本地代码检索及动态歌词展示,帮助开发者根据实际需求筛选工具并正确验证结果。

    opencode安装skills教程及本地配置方法
    opencode安装skills教程及本地配置方法

    遇到OpenCode Skills无法加载或配置无效?本文手把手教你完成Skills的安装、环境变量设置及本地调试,避开常见坑点,让AI助手更懂你的代码习惯。

    如何开启Visual Studio2022实时代码分析警告提示
    如何开启Visual Studio2022实时代码分析警告提示

    解决 Visual Studio 2022 编辑时不显示代码分析警告的问题。指南涵盖 .NET 项目实时分析设置、Roslyn 分析器范围调整及错误列表来源筛选,确保开发者能高效利用实时代码诊断功能。

    如何在Visual Studio2022查看变量监视窗口调试程序
    如何在Visual Studio2022查看变量监视窗口调试程序

    详解 Visual Studio 2022 调试技巧:包括断点设置、监视窗口开启方法、变量添加步骤以及通过 F10 单步执行观察变量值变化的全过程。

    Visual Studio2017设置默认编码为UTF-8不带BOM
    Visual Studio2017设置默认编码为UTF-8不带BOM

    针对 Visual Studio 2017 缺乏全局默认编码选项的情况,详解如何利用 .editorconfig 设定项目级 UTF-8 无 BOM 规则,以及手动转换现有文件并校验字节的方法。

    Visual Studio2022F5调试提示启动项目未配置怎么修复
    Visual Studio2022F5调试提示启动项目未配置怎么修复

    解决 Visual Studio 2022 F5 调试报错问题,通过设置启动项目、检查解决方案属性及正确区分类库与应用项目,快速恢复调试功能。

    Visual Studio2022NuGet包还原失败离线处理方案
    Visual Studio2022NuGet包还原失败离线处理方案

    解决Visual Studio 2022离线环境中NuGet包还原失败的难题,涵盖依赖链分析、本地源配置、包集收集及隔离验证步骤,帮助开发者构建稳定的离线开发环境。

    Visual Studio2017大项目加载缓慢,优化加载性能技巧
    Visual Studio2017大项目加载缓慢,优化加载性能技巧

    解决Visual Studio 2017大型项目加载慢的实用指南。涵盖性能监测、扩展管理、文档恢复优化及版本兼容性检查,帮助开发者精准定位瓶颈并提升工作效率。

    Visual Studio2022设置多启动项目同时调试运行
    Visual Studio2022设置多启动项目同时调试运行

    详解 Visual Studio 2022 多启动项目配置方法,包括启动动作选择、顺序调整及调试技巧,帮助开发者高效调试分布式或前后端分离应用。

    fastapi下载文件接口实现教程
    fastapi下载文件接口实现教程

    深入解析在FastAPI框架中实现文件下载接口的完整方案,涵盖基于FileResponse的本地静态文件传输、基于StreamingResponse的分块流式传输、RFC 5987中文文件名编码规范、路径穿越安全防御及BackgroundTasks临时文件清理,并配备详细代码示例与架构解析图。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。

    为什么要购买正版软件?
    01 官方授权

    授权来源透明,订单与许可信息可随时查询。

    02 即时交付

    付款成功后自动发放激活信息,支持订单追踪。

    03 中文协助

    安装、激活、迁移与续费问题都有专人响应。

    04 长期可用

    提供升级提醒与授权管理,换机时也能轻松迁移。

    网站备案号:苏ICP备2026018738号-1 联系邮箱:bd@zhengruan.com 网站地图

    Copyright ©2018-2026