首页/文章/ 详情

软件开发—「C++开发技巧之---智能指针使用技巧 」

9月前浏览572
在 C++ 中,智能指针是管理动态分配内存、防止内存泄漏和简化资源管理的核心工具。它们通过 RAII(Resource Acquisition Is Initialization)原则,在构造时获取资源,在析构时自动释放资源。正确使用智能指针可以显著提高代码的安全性和可维护性。以下是 C++ 智能指针的使用技巧和最佳实践: 
     

 理解并选择合适的智能指针类型📌    


   

   

   

   
  • std::unique_ptr<T>(C++11 起):

    • 核心概念:独占所有权。同一时间只有一个 unique_ptr可以指向该对象。

    • 所有权转移: 只能通过移动语义 (std::move) 转移所有权。不能复 制

    • 轻量级: 开销很小,通常只比原始指针多一点点(可能包含一个自定义删除器)。

    • 使用场景:

      • 管理具有明确单一所有者的对象。

      • 作为工厂函数的返回值。

      • 在容器中存储动态分配的对象(优先于 shared_ptr)。

      • 实现 Pimpl 惯用法

  • 技巧:

    • 优先使用 std::make_unique<T>(args)(C++14 起): 更安全(防止内存泄漏异常)、更高效(减少一次内存分配)、更简洁。

    • 自定义删除器: 可以管理非内存资源(如文件句柄 FILE*、套接字等)。unique_ptr的自定义删除器是类型的一部分,影响其类型。

    • 释放所有权: 使用 release()方法获取原始指针并释放所有权(谨慎使用!通常意味着你需要手动管理该资源了)。

    • 重置资源: 使用 reset(new_ptr)或 reset()

    • 检查是否为空: 使用 if (ptr)或 if (ptr != nullptr)

  • std::shared_ptr<T>(C++11 起):

    • 核心概念:共享所有权。多个 shared_ptr可以指向同一个对象。对象在其最后一个 shared_ptr被销毁(或重置)时销毁。

    • 实现: 使用引用计数。控制块(包含引用计数、弱引用计数、自定义删除器等)通常与对象本身分开分配(除非使用 make_shared)。

    • 所有权共享: 可以通过复 制构造函数或赋值操作符共享所有权。

    • 开销: 比 unique_ptr大,因为需要维护引用计数和控制块。

    • 使用场景:

      • 对象需要被多个不相关的部分共享,且没有明确的单一所有者。

      • 需要将指针存储在容器中,并且容器中的元素可能被复 制或赋值(但优先考虑 unique_ptr+ 容器存储 unique_ptr)。

      • 需要延迟销毁(例如缓存)。

  • 技巧:

    • 绝对优先使用 std::make_shared<T>(args): 提供与 make_unique相同的安全性优势,并且通常更高效,因为它可以将对象内存和控制块内存合并为一次分配

    • 避免从原始指针直接构造多个 shared_ptr: 这会导致多个独立的引用计数,对象会被多次删除,引发未定义行为(通常是崩溃)。总是从一个原始的 new表达式初始化第一个 shared_ptr,然后通过复 制这个 shared_ptr来创建新的共享指针。

    • 谨慎使用 shared_ptr<T>(new T(args)): 只有在无法使用 make_shared时才用(例如需要自定义分配器或删除器,或者构造函数是私有的且需要友元 make_shared)。注意潜在的异常安全问题(如果 new成功但 shared_ptr构造失败,内存会泄漏)。

    • 自定义删除器: 可以指定。与 unique_ptr不同,删除器不是 shared_ptr类型的一部分(通过类型擦除实现)。

    • shared_from_this: 如果一个类需要返回自身的 shared_ptr(例如在成员函数中),该类需要继承 std::enable_shared_from_this<T>,并调用 shared_from_this()成员函数。前提是对象必须已经由一个 shared_ptr管理。

    • 避免循环引用: 这是 shared_ptr最常见的内存泄漏原因!见下文的 weak_ptr

  • std::weak_ptr<T>(C++11 起):

    • 核心概念:弱引用。它指向一个由 shared_ptr管理的对象,但不增加引用计数。它不拥有对象的所有权。

    • 目的: 解决 shared_ptr循环引用问题,以及安全地访问可能已被销毁的对象。

    • 使用场景:

      • 打破 shared_ptr的循环引用(例如,在双向链表、观察者模式、缓存实现中)。

      • 需要观察一个对象是否存在,但不阻止其销毁。

  • 技巧:

    • 必须从一个 shared_ptr或另一个 weak_ptr构造/赋值。

    • 检查对象是否存在并获取 shared_ptr: 使用 lock()方法。它返回一个 shared_ptr。如果对象还存在(引用计数 > 0),则返回一个有效的 shared_ptr(增加引用计数);如果对象已被销毁,则返回一个空的 shared_ptr。这是最安全的使用方式。

    • 检查是否过期: 使用 expired()方法(但注意在多线程环境下,expired()返回 false后调用 lock()可能仍然返回空,因为对象可能刚被销毁。通常直接使用 lock()更可靠)。

    • 获取原始 shared_ptr: 可以使用构造函数 shared_ptr<T>(weak_ptr<T>),但如果 weak_ptr已过期(对象已销毁),此构造函数会抛出 std::bad_weak_ptr异常。

     

通用技巧和最佳实践📌     


   

   

   

   
  • 优先使用智能指针,避免 new和 delete: 让智能指针管理对象的生命周期。显式的 delete往往是错误和泄漏的来源。

  • 优先选择 unique_ptr: 默认使用 unique_ptr。它更轻量、语义更清晰(单一所有者)。只有在确实需要共享所有权时才使用 shared_ptr。过度使用 shared_ptr会增加开销并可能导致循环引用。

  • 优先使用 make_unique和 make_shared: 它们提供了更强的异常安全保证(防止内存泄漏),并且 make_shared通常更高效(单次内存分配)。

  • 避免混合使用智能指针和原始指针管理同一资源: 这极易导致双重删除或访问已释放内存。

  • 谨慎传递原始指针或引用: 当函数只需要使用对象而不需要管理其生命周期时,优先传递原始指针 (T*) 或引用 (T&),而不是智能指针本身(尤其是 shared_ptr)。传递 shared_ptr会不必要地增加引用计数,延长对象生命周期,并可能引入意外的所有权共享。使用 get()方法获取原始指针用于观察。

  • 明确所有权语义:

    • 函数获取资源所有权:使用 unique_ptr作为参数(按值传递,表示所有权转移)或返回 unique_ptr

    • 函数共享资源所有权:使用 shared_ptr作为参数(按值传递表示共享所有权)或返回 shared_ptr

    • 函数借用资源(只使用,不管理):使用 T*或 T&

  • 在容器中使用智能指针: 使用 vector<std::unique_ptr<T>>或 vector<std::shared_ptr<T>>来管理动态分配对象的集 合。unique_ptr在容器中通常是更好的选择,除非元素需要被多个容器或外部实体共享。

  • 处理多态和基类指针:

    • 智能指针支持多态。unique_ptr<Base>可以指向 Derived对象。

    • 确保基类析构函数是 virtual的!否则通过基类指针删除派生类对象是未定义行为。

  • 注意数组:

    • std::unique_ptr<T[]>有对数组的偏特化版本,会调用 delete[]。使用 make_unique<T[]>(size)创建动态数组。

    • std::shared_ptr<T>没有内置的数组支持。使用 shared_ptr管理数组需要提供自定义删除器(例如 std::default_delete<T[]>或 [](T* p) { delete[] p; })。C++17 引入了 std::shared_ptr<T[]>,但使用不如 unique_ptr的数组版本方便。通常优先考虑 std::vector或 std::array

  • 跨模块/DLL 边界: 在动态库(DLL/SO)中创建和删除智能指针管理的对象时,要确保对象在同一个模块内分配和释放。否则可能导致运行时错误(因为堆的实现可能不同)。一种常见做法是提供工厂函数(返回 unique_ptr)和明确的销毁函数(如果需要跨模块传递所有权,有时需约定使用特定的分配/释放函数)。

  • 性能考量:

    • unique_ptr: 开销极小,几乎等同于原始指针。

    • shared_ptr: 构造/复 制/销毁涉及引用计数的原子操作(线程安全保证),有一定开销。make_shared可以减少一次内存分配的开销。

    • weak_ptr: 操作通常也涉及原子操作。

    • 在性能极度敏感的代码路径中,如果所有权清晰且生命周期严格可控,经过仔细评估后,有时使用原始指针(配合良好的设计规范)可能是合理的,但这通常是例外而非规则。务必权衡安全性和性能。

  • 循环引用是 shared_ptr的大敌: 如果两个或多个 shared_ptr相互持有对方的引用(例如,父对象持有子对象的 shared_ptr,子对象又持有父对象的 shared_ptr),它们的引用计数永远不会降到零,导致内存泄漏。解决方案: 将其中一个方向的引用改为 weak_ptr(通常是子对象指向父对象的引用)。

     

 总结📌    


   

   

   
  • 核心原则:

    💡 熟练掌握智能指针是编写现代、安全、高效 C++ 代码的关键。始终优先考虑资源所有权的清晰性,并利用智能指针自动管理生命周期的能力来避免常见的内存管理错误。

    1. 让编译器管理资源: 尽可能使用智能指针替代 new/delete

    2. 默认选择 unique_ptr 只在确需共享所有权时才用 shared_ptr

    3. 工厂函数优先: 创建时首选 make_unique/make_shared

    4. 传递权限而非所有权: 函数仅需访问资源时,使用原始指针或引用。

    5. 警惕循环引用: 使用 weak_ptr破解相互持有的 shared_ptr

    6. 容器存储: 用 vector<unique_ptr<T>>管理对象集 合更安全高效。


来源:IFD优飞迪
二次开发通用电子云计算数字孪生控制工厂
著作权归作者所有,欢迎分享,未经许可,不得转载
首次发布时间:2025-11-09
最近编辑:9月前
优飞迪科技
赋能新仿真,创优新设计
获赞 323粉丝 349文章 520课程 4
点赞
收藏
作者推荐
未登录
还没有评论
课程
培训
服务
行家
VIP会员 学习计划 福利任务
下载APP
联系我们
帮助与反馈