详细介绍:《C++进阶之C++11》【智能指针】(下)

在这里插入图片描述

往期《C++初阶》回顾:

《C++初阶》目录导航


往期《C++进阶》回顾:
/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】
【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】
【多态:概念 + 实现 + 拓展 + 原理】
/------------ STL ------------/
【二叉搜索树】
【AVL树】
【红黑树】
【set/map 使用介绍】
【set/map 模拟实现】
【哈希表】
【unordered_set/unordered_map 使用介绍】
【unordered_set/unordered_map 模拟实现】
/------------ C++11 ------------/
【列表初始化 + 右值引用】
【移动语义 + 完美转发】
【可变参数模板 + emplace接口 + 新的类功能】
【lambda表达式 + 包装器】
【异常】
【智能指针】(上)

前言:

hi ~ 小伙伴们大家好啊啊啊啊啊!(ノ≧∀≦)ノ✨ 今天正是中秋节(你看到这篇博客的时候),在这么个团圆的日子里,一秒不见甚是想念,所以鼠鼠马不停蹄把 【智能指针】(下) 给大家带过来啦!

对了,这篇博客就是咱们 C++ 系列的最后一篇了!从今年儿童节那天,鼠鼠发出 C++ 系列的第一篇博客【C++ 的前世今生】,到今天中秋节发的最后一篇 【智能指针】(下),其实正好是128天哦~,终于可以郑重地跟大家说一句:C++ 系列,完结撒花啦,哈哈哈~(。・ω・。)ノ♡

这 128 天里,真的特别特别感谢小伙伴们的一路支持!不管是默默看文、点赞收藏,还是偶尔留言互动,都让鼠鼠觉得敲代码、写博客的日子特别有劲儿,也让我更有动力把每个知识点讲明白~

嘤嘤嘤,先别走呀!(=゚ω゚)ノ鼠鼠还有重要的事儿没说呢~ 接下来呢,鼠鼠就要开启新的内容系列啦 —— 《Linux 系统》!

不过这里也得跟大家坦诚说一句:Linux 这部分内容是真的不简单,不仅知识点杂、零散的细节多,想要挖深讲透也得花不少功夫。所以鼠鼠写 Linux 这部分的博客,会比之前慢不少。(´• ω •`;)

虽然鼠鼠已经写好一部分 Linux 的文章了,但就算有存稿,也不能一下子发出来。( ̄▽ ̄*)ゞ

因为鼠鼠现在学的进度已经落后了,接下来一段时间重心将放在推进课程学习上了,这样一来,能留给写博客的时间就会大幅压缩了,更新会比较的慢(虽然就没快过哈哈哈)( ̄﹏ ̄;)

但请大家放心,慢归慢,这部分的内容质量绝不会打折扣!这部分 Linux 的内容,鼠鼠将会花更多心思打磨,写得更细致、更深入,争取让每个看的小伙伴都能实实在在学到东西~(。•̀ᴗ-)✧

------------ 智能指针的原理 ------------

auto_ptr

一、基本介绍

auto_ptr(已废弃,理解原理即可)

auto_ptr是 C++98 时期的早期智能指针,核心设计是 “拷贝时转移资源所有权”

  • 当拷贝 auto_ptr 时,原指针会失去资源所有权(变为悬空指针),新指针接管资源
  • 这种设计极易引发逻辑错误(悬空指针访问、重复释放),因此 C++11 后已被彻底弃用

使用示例

auto_ptr<int> p1(new int(10));
  auto_ptr<int> p2 = p1; // p1 失去所有权 → 变为悬空指针
    *p1; // 未定义行为(访问空悬指针)

重要结论auto_ptr 的设计思路存在根本缺陷,禁止在现代代码中使用

二、模拟实现

/*-------------------------------------- auto_ptr 模拟实现(已废弃) --------------------------------------*/
template<class T>
  class auto_ptr
  {
  private:
  T* _ptr; // 指向管理的动态资源(如 :new分配的对象)
  public:
  // 1. 构造函数:接收外部new的资源指针,接管所有权
  auto_ptr(T* ptr)
  : _ptr(ptr)
  { }
  // 2. 析构函数:当auto_ptr对象生命周期结束时,自动释放管理的资源
  ~auto_ptr()
  {
  if (_ptr)  // 确保指针非空,避免重复释放
  {
  //1.显示释放的地址
  cout << "delete:" << _ptr << endl;
  //2.释放动态资源
  delete _ptr;
  //3.指针置空避免野指针
  _ptr = nullptr;
  }
  }
  // 3. 拷贝构造函数 ---> 采用"所有权转移"策略 ---> 原对象会失去资源所有权,变为悬空指针
  auto_ptr(auto_ptr<T>& sp)
    : _ptr(sp._ptr)     //1.新对象接管原对象的资源
    {
    sp._ptr = nullptr;  //2.原对象指针置空,失去所有权
    }
    // 4. 拷贝赋值运算符 ---> 采用"所有权转移"策略
    auto_ptr<T>& operator=(auto_ptr<T>& ap)
      {
      //1.检测自赋值以避免自己赋值给自己导致的错误
      if (this != &ap)
      {
      //第一步:先释放当前资源
      if (_ptr)
      {
      delete _ptr;
      }
      //第二步:再接管新资源
      _ptr = ap._ptr;  //接管ap的资源
      //第三步:最后将原对象指针置空
      ap._ptr = nullptr;  // ap失去所有权
      }
      //2.返回当前对象以支持链式赋值
      return *this;
      }
      // 5. 重载解引用运算符:
      T& operator*()
      {
      return *_ptr;  // 返回资源的引用
      }
      //注意:模拟原始指针的*操作,允许通过auto_ptr访问“资源本身”
      // 6. 重载成员访问运算符
      T* operator->()
      {
      return _ptr;  // 返回资源指针
      }
      //注意:模拟原始指针的->操作,允许通过auto_ptr访问“资源的成员”
      };

unique_ptr

一、基本介绍

unique_ptr(C++11 推荐,独占所有权)

unique_ptr解决了 auto_ptr 的缺陷,核心设计是“禁止拷贝,支持移动”

  • 资源所有权唯一,拷贝会触发编译报错(避免悬空指针)
  • 支持移动语义(std::move),转移所有权后原指针变为空

使用示例

unique_ptr<int> p1(new int(10));
  // unique_ptr<int> p2 = p1;    // 编译报错(禁止拷贝)
    unique_ptr<int> p2 = move(p1); // 移动语义,p1 置空,p2 接管资源

二、模拟实现

/*-------------------------------------- unique_ptr 模拟实现(独占所有权) --------------------------------------*/
template<class T>
  class unique_ptr
  {
  private:
  T* _ptr;  // 指向管理的动态资源
  public:
  // 1. 构造函数
  explicit unique_ptr(T* ptr) // explicit:禁止隐式转换(避免int*等意外转换为unique_ptr)
  : _ptr(ptr)
  { }
  /* 对比分析:unique_ptr和auto_ptr的“构造函数”的区别
  *      1. unique_ptr的构造函数使用了explicit 关键字进行了禁止隐式转换
  */
  // 2. 析构函数
  ~unique_ptr()
  {
  if (_ptr)
  {
  //1.
  cout << "delete:" << _ptr << endl;
  //2.
  delete _ptr;
  //3.
  _ptr = nullptr;
  }
  }
  //对比分析:这里的unique_ptr和auto_ptr的“析构函数”实现一样
  // 3. 禁止拷贝:删除“拷贝构造和拷贝赋值”---> 确保资源所有权唯一,避免重复释放
  //3.1:禁止拷贝构造
  unique_ptr(const unique_ptr<T>& sp) = delete;
    //3.2:禁止拷贝赋值
    unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
      // 4. 重载解引用运算符
      T& operator*()
      {
      return *_ptr;
      }
      //对比分析:这里的unique_ptr和auto_ptr的“重载解引用运算符”实现一样
      // 5. 重载成员访问运算符
      T* operator->()
      {
      return _ptr;
      }
      //对比分析:这里的unique_ptr和auto_ptr的“重载成员访问运算符”实现一样
      // 6. 移动构造函数 ---> 通过右值引用转移所有权
      unique_ptr(unique_ptr<T>&& sp)
        : _ptr(sp._ptr)     //1.新对象接管原对象的资源
        {
        sp._ptr = nullptr;  //2.原对象指针置空,失去所有权
        }
        /* 对比分析:auto_ptr的“拷贝构造函数”和 unique_ptr的“移动构造函数”的唯一区别:函数参数的不同
        *      1. auto_ptr(auto_ptr<T>& sp)
        *      2. unique_ptr:unique_ptr(unique_ptr<T>&& sp)
        */
        // 7. 移动赋值运算符 ---> 通过右值引用转移所有权
        unique_ptr<T>& operator=(unique_ptr<T>&& sp)
          {
          //1.检测自赋值以避免自己赋值给自己导致的错误
          if (this != &sp)
          {
          //第一步:先释放当前资源
          if (_ptr)
          {
          delete _ptr;
          }
          //第二步:再接管新资源
          _ptr = sp._ptr;  //接管sp的资源
          //第三步:最后将原对象指针置空
          sp._ptr = nullptr;  // sp置空失去所有权
          }
          //2.返回当前对象以支持链式赋值
          return *this;
          }
          /* 对比分析:auto_ptr的“拷贝赋值运算符”和 unique_ptr的“移动赋值运算符”的唯一区别:函数参数的不同
          *      1. auto_ptr:auto_ptr<T>& operator=(auto_ptr<T>& ap)
          *      2. unique_ptr:unique_ptr<T>& operator=(unique_ptr<T>&& sp)
          */
          };

shared_ptr

一、基本介绍

shared_ptr(C++11 推荐,共享所有权)

shared_ptr的核心挑战是 “共享资源的引用计数管理”

  • 多个 shared_ptr 可共享同一资源,需通过引用计数跟踪资源的 “活跃引用者数量”
  • 计数为 0 时,自动释放资源

使用示例

shared_ptr<int> p1(new int(10)); // 计数 = 1
  shared_ptr<int> p2 = p1;         // 计数 = 2(共享同一计数空间)
    // p1 和 p2 析构时,计数依次减为 1 → 0 → 释放资源

二、模拟实现

/*-------------------------------------- shared_ptr 模拟实现(共享所有权) --------------------------------------*/
template<class T>
  class shared_ptr
  {
  private:
  //1.指向共享的资源的指针
  //2.引用计数(堆上分配,所有共享对象共享)
  //3.自定义删除器(支持数组/文件句柄等资源)
  T* _ptr;
  int* _pcount;
  function<void(T*)> _del;
    public:
    //1.实现:“默认构造函数”(支持默认删除器)
    explicit shared_ptr(T* ptr = nullptr)
    : _ptr(ptr)                       //1.管理空资源
    , _pcount(new int(1))              //2.计数在堆上,初始值1
    , _del([](T* ptr) { delete ptr; }) //3.lambda作为默认删除器
    { }
    /* 对比分析:auto_ptr和unique_ptr、shared_ptr的“构造函数”的区别
    *      1. unique_ptr的构造函数使用了explicit 关键字进行了禁止隐式转换
    *      2. shared_ptr除了要管理动态资源,还要管理“引用计数 + 删除器”
    */
    //2.实现:“普通构造函数”(带自定义删除器)---> 管理数组(需delete[])、文件句柄(需fclose)等非普通指针
    shared_ptr(T* ptr, function<void(T*)> del)
      : _ptr(ptr)              //1.管理共享的资源
      , _pcount(new int(1))     //2.计数在堆上,初始值1
      , _del(del)               //3.接收自定义删除器
      { }
      //3.实现:“释放资源”的核心逻辑:计数-1,为0时彻底释放
      void release()
      {
      if (--(*_pcount) == 0)  //注意细节:调用“释放资源”一定会进行计数-1
      {                       //但是只有计数减为0时,说明是最后一个引用者,才会进行资源释放
      //1.调用删除器释放资源
      _del(_ptr);
      //2.释放计数空间
      delete _pcount;
      //3.避免野指针
      _ptr = nullptr;       // 将指向“共享资源”的指针置空
      _pcount = nullptr;    // 将指向“计数空间”的指针置空
      }
      }
      /* 对比分析:auto_ptr和unique_ptr、shared_ptr的“析构函数”的区别
      *      1. unique_ptr和auto_ptr的“析构函数”实现一样
      *
      * ------------------------------------------------------------------------
      *  与shared_ptr的区别:
      *      1. 前两个智能指针释放资源之前要判断:当前指向资源的指针是否为空,避免重复释放
      *      2. shared_ptr指针释放资源之前要进行:“先:引用计数-1 + 后:判断引用数是否为0”
      *    --------------------------------------------------------------------------------
      *      1. 前两个智能指针使用delete释放资源:delete _ptr
      *      2. shared_ptr指针使用删除器释放资源:_del(_ptr)
      *    --------------------------------------------------------------------------------
      *      1. 前两个智能指针只需要将:指向资源的指针置空即可
      *      2. shared_ptr智能指针需要:“指向动态资源 + 指向引用空间”的指针
      */
      //4.实现:“析构函数”
      ~shared_ptr()
      {
      release();  //调用release释放资源
      }
      //5.实现:“拷贝构造函数”
      shared_ptr(const shared_ptr<T>& sp)
        : _ptr(sp._ptr),      //1.共享资源
        _pcount(sp._pcount),  //2.共享计数
        _del(sp._del)         //3.共享删除器
        {
        (*_pcount)++;         //4.引用计数+1
        } //注意:每调用一次“拷贝构造函数”就会进行一次“引用计数+1”
        /* 对比分析:auto_ptr和shared_ptr的“拷贝构造函数”的区别:
        *      1. auto_ptr只需新对象管理原对象的:动态资源
        *      2. shared_ptr需新对象管理原对象的:“动态资源 + 引用计数 + 删除器”
        * ------------------------------------------------------------------------
        *      1. auto_ptr在函数体中需要:将原对象指针置空 (核心特点)
        *      2. shared_ptr在函数体中需:“引用计数+1”   (核心特点)
        */
        //6.实现:“拷贝赋值运算符”:释放当前资源,共享新资源
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
          {
          //1.检测自赋值以避免自己赋值给自己导致的错误
          if (_ptr != sp._ptr) //若资源不同,才需要释放当前资源
          {
          //1.释放当前资源(计数-1)
          release();
          //2.共享新资源
          _ptr = sp._ptr;
          _pcount = sp._pcount;
          _del = sp._del;
          //3.新资源计数+1
          (*_pcount)++;
          }
          //2.返回当前对象以支持链式赋值
          return *this;
          }
          /* 对比分析:auto_ptr和shared_ptr的“拷贝赋值运算符”的区别:
          *      1. auto_ptr检测自赋值的方式是:if(this != &ap)
          *      2. shared_ptr检测自赋值的方式:if(_ptr != sp._ptr)
          * ------------------------------------------------------------------------
          *      1. auto_ptr在函数体中需要:将原对象指针置空 (核心特点)
          *      2. shared_ptr在函数体中需:“引用计数+1”   (核心特点)
          * ------------------------------------------------------------------------
          *      1. auto_ptr拷贝赋值的第二步“接管新资源”:只需接管原对象的_ptr
          *      2. shared_ptr拷贝赋值的第二步“共享新资源”:需要共享“_ptr + _pcont + _del”
          * ------------------------------------------------------------------------
          *      1. auto_ptr拷贝赋值的第三步:将原对象置空:ap._ptr = nullptr
          *      2. shared_ptr拷贝赋值第三步:新资源计数+1:(*_pcount)++
          */
          //7.实现:“重载解引用运算符”
          T& operator*()
          {
          return *_ptr;
          }
          //8.实现:“重载成员访问运算符”
          T* operator->()
          {
          return _ptr;
          }
          //9.实现:“获取原始指针”
          T* get() const
          {
          return _ptr;
          }
          //10.实现:“获取当前引用计数”
          int use_count() const
          {
          return *_pcount;  // 返回计数的值
          }
          };

1. function<void(T*)> _del; 怎么理解?

template<class T>
  class shared_ptr
  {
  private:
  //1.指向共享的资源的指针
  //2.引用计数(堆上分配,所有共享对象共享)
  //3.自定义删除器(支持数组/文件句柄等资源)
  T* _ptr;
  int* _pcount;
  function<void(T*)> _del;
    }

function<void(T*)> _del;shared_ptr 中用于自定义资源释放逻辑的成员变量,它的设计体现了 shared_ptr 对复杂资源管理的灵活性。


类型解析:std::function<void(T*)>

  • std::function:是 C++11 引入的 通用函数包装器,可以存储、复制和调用任何可调用对象(函数指针、函数对象、lambda 表达式等)
  • void(T*):表示这个函数包装器接受一个 T* 类型的参数,且没有返回值(void

简单说_del 是一个 “函数容器”,里面装着一段 “如何释放 T* 类型资源” 的逻辑。


核心作用:突破默认 delete 的限制

shared_ptr 的默认行为是用 delete 释放资源,但实际开发中需要管理的资源可能不止 “new 分配的单个对象”,例如:

  • 动态数组(需要 delete[] 释放)
  • 文件句柄(需要 fclose 关闭)
  • 网络连接(需要 close 断开)

简单说_del 的存在就是为了让用户自定义释放逻辑,替代默认的 delete


代码中的使用场景

(1)默认删除器(单个对象)

  • 在默认构造函数中,_del 被初始化为一个 lambda 表达式

  • 这表示如果用户不指定释放逻辑,默认用 delete 释放 T* 类型的单个对象

    _del([](T* ptr) { delete ptr; })

(2)自定义删除器(动态数组)

  • 当管理动态数组时,用户可以传入 delete[] 的释放逻辑

  • 此时 _del 存储的是 delete[] 逻辑,析构时会正确释放数组

    // 定义数组删除器(函数或lambda)
    auto delArray = [](int* ptr) { delete[] ptr; };
    // 用自定义删除器构造shared_ptr
    shared_ptr<int> sp(new int[10], delArray);

(3)自定义删除器(文件句柄)

  • 管理文件资源时,释放逻辑是 fclose

  • 析构时 _del 会调用 fclose 而非 delete,避免资源泄漏

    // 文件删除器:关闭文件句柄
    auto delFile = [](FILE* fp) { fclose(fp); };
    // 管理文件资源
    shared_ptr<FILE> sp(fopen("test.txt", "r"), delFile);

2. 引用计数int* _pcount;为什么用指针类型?

template<class T>
  class shared_ptr
  {
  private:
  //1.指向共享的资源的指针
  //2.引用计数(堆上分配,所有共享对象共享)
  //3.自定义删除器(支持数组/文件句柄等资源)
  T* _ptr;
  int* _pcount;
  function<void(T*)> _del;
    }

shared_ptr 中,引用计数 int* _pcount 采用指针类型(而非普通 int 类型),是实现资源共享的核心设计。


shared_ptr 的核心特性是多个对象共享同一资源,而引用计数的作用是跟踪 “当前有多少个 shared_ptr 在共享该资源”。

  • 若 _pcount 是普通 int 变量(非指针)每个 shared_ptr 会有自己的计数副本

    // 错误示例:_pcount 为普通 int
    shared_ptr<Date> sp1(new Date);  // sp1._pcount = 1
      shared_ptr<Date> sp2(sp1);       // sp2._pcount = sp1._pcount(副本,值为1)

    此时 sp1sp2 的计数是独立的,无法同步更新,导致引用计数失去意义。

  • 若 _pcount 是指针(指向堆上的 int)所有共享者会指向同一块计数内存

    // 正确设计:_pcount 为指针
    shared_ptr<Date> sp1(new Date);  // sp1._pcount 指向堆内存(值为1)
      shared_ptr<Date> sp2(sp1);       // sp2._pcount 与 sp1._pcount 指向同一块内存(值为2)

    此时无论哪个 shared_ptr 修改计数(++--),所有共享者都能看到最新值,确保计数准确。


总结:

int* _pcount 用指针类型的核心原因是
让所有共享同一资源的 shared_ptr 实例,能够访问和修改同一块计数内存,确保引用计数的准确性和一致性

这一设计是 shared_ptr 实现 “共享所有权” 的基础,也是其区别于 unique_ptr(独占所有权,无需计数)的关键技术点。

3. 为什么使用if(_ptr!= sp._ptr)进行检测自赋值?

//6.实现:“拷贝赋值运算符”:释放当前资源,共享新资源
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
  {
  //1.检测自赋值以避免自己赋值给自己导致的错误
  if (_ptr != sp._ptr) //若资源不同,才需要释放当前资源
  {
  //1.释放当前资源(计数-1)
  release();
  //2.共享新资源
  _ptr = sp._ptr;
  _pcount = sp._pcount;
  _del = sp._del;
  //3.新资源计数+1
  (*_pcount)++;
  }
  //2.返回当前对象以支持链式赋值
  return *this;
  }

shared_ptr 的拷贝赋值运算符中,使用 if (_ptr != sp._ptr) 检测自赋值,是由其共享资源的特性决定的,与 auto_ptrif (this != &ap) 检测自赋值的逻辑有本质区别。


先明确:什么是 “自赋值”?

  • 自赋值不仅指 “自己给自己赋值”(如:sp = sp

  • 还包括 “两个指向同一资源的 shared_ptr 互相赋值”(如:sp1 = sp2,且 sp1sp2 原本就共享同一资源)

这两种情况都属于 “自赋值场景”,需要通过检测避免错误。


为什么不用 if (this != &sp)?

if (this != &sp) 只能检测“完全相同的对象给自己赋值”(如:sp = sp),但无法检测 “不同对象但共享同一资源” 的情况。

shared_ptr<Date> sp1(new Date);
  shared_ptr<Date> sp2(sp1);  // sp1和sp2共享同一资源(_ptr相同)
    sp1 = sp2;  // 此时this != &sp(sp1和sp2是不同对象),但属于自赋值

若用 if (this != &sp),会认为这不是自赋值,进而执行后续逻辑,导致错误。


为什么 if (_ptr != sp._ptr) 更合理?

shared_ptr 的核心是共享资源,判断是否为 “自赋值” 的关键是:两个 shared_ptr 是否指向同一资源,而非是否为同一个对象。

  • 当_ptr == sp._ptr时:表示两者共享同一资源,赋值操作无意义(不会改变资源),且执行后续逻辑会导致错误
  • 当_ptr != sp._ptr时:表示两者指向不同资源,需要执行 “释放当前资源 + 共享新资源” 的逻辑

不检测会导致什么问题?

  • 若跳过 if (_ptr != sp._ptr),直接执行赋值逻辑,会发生以下错误:

    // 假设sp1和sp2共享同一资源(_ptr相同)
    sp1 = sp2;
    /*------------------------- 执行赋值逻辑 -------------------------*/
    // 第一步:释放当前资源(计数-1)
    release();   // 此时计数可能减为0,资源被释放
    // 第二步:共享新资源(但sp2._ptr已被释放,成为野指针)
    _ptr = sp2._ptr;
    // 第三步:计数+1(操作已释放的内存,未定义行为)
    (*_pcount)++;
  • 后果:同一资源被提前释放,后续操作野指针导致崩溃。


5. 本质:shared_ptr 的 “身份” 由资源决定

shared_ptr 的核心是管理资源,其 “身份” 由所指向的资源(_ptr)决定,而非对象本身(this

因此检测自赋值的逻辑必须围绕 “资源是否相同”(_ptr 比较),而非 “对象是否相同”(this 比较)


总结:智能指针的设计取舍

智能指针核心设计适用场景缺陷 / 注意事项
auto_ptr拷贝转移所有权(已废弃)空悬指针、重复释放风险
unique_ptr禁止拷贝,支持移动资源独占场景(如局部对象)无明显缺陷,现代首选
shared_ptr引用计数共享资源多对象共享资源场景引用计数有开销,需避免循环引用

通过模拟实现,能更深刻理解智能指针的设计思想:

  • unique_ptr禁止拷贝保证资源安全
  • shared_ptr堆上的引用计数实现共享管理

weak_ptr

一、基本介绍

weak_ptr(C++11 引入,弱引用辅助)

weak_ptr是为辅助 shared_ptr 解决循环引用问题设计的弱引用智能指针,核心特点是 “不参与引用计数,仅观察资源”

  • 绑定到 shared_ptr 时不增加引用计数,不影响资源的释放时机
  • 需通过 lock() 转换为 shared_ptr 才能安全访问资源(避免访问已释放的悬空指针)
  • 专门用于打破 shared_ptr 形成的循环引用,让引用计数逻辑回归正常

使用示例:(配合循环引用场景)

#include <iostream>
  #include <memory>
    using namespace std;
    struct Node
    {
    // 用 weak_ptr 替代 shared_ptr 打破循环
    weak_ptr<Node> _next;
      weak_ptr<Node> _prev;
        ~Node()
        {
        cout << "~Node()" << endl;
        }
        };
        int main()
        {
        shared_ptr<Node> n1(new Node); // 计数 = 1
          shared_ptr<Node> n2(new Node); // 计数 = 1
            n1->_next = n2; // weak_ptr 不增加计数,n2 计数仍为 1 
            n2->_prev = n1; // weak_ptr 不增加计数,n1 计数仍为 1 
            // 离开作用域时:
            // n1 析构 → 计数 1-1=0 → 释放节点,_next 自动置空  
            // n2 析构 → 计数 1-1=0 → 释放节点,_prev 自动置空  
            }

在这里插入图片描述

关键对比:(与 shared_ptr 配合)

#include <iostream>
  #include <memory>
    using namespace std;
    struct Node
    {
    // 用 weak_ptr 替代 shared_ptr 打破循环
    weak_ptr<Node> _next;
      weak_ptr<Node> _prev;
        ~Node()
        {
        cout << "~Node()" << endl;
        }
        };
        int main()
        {
        shared_ptr<Node> sp(new Node);
          weak_ptr<Node> wp(sp);
            cout << sp.use_count() << endl; // 输出 1(weak_ptr 不影响计数)
            if (shared_ptr<Node> tmp = wp.lock())
              {
              // lock() 成功:资源存活,tmp 是有效的 shared_ptr(计数临时+1)
              cout << "资源可用" << endl;
              }
              else
              {
              // lock() 失败:资源已释放,避免访问悬空指针
              cout << "资源已释放" << endl;
              }
              return 0;
              }

在这里插入图片描述

重要结论

weak_ptrshared_ptr“辅助工具”,自身不管理资源生命周期:

  • 解决 shared_ptr 循环引用导致的内存泄漏问题
  • 作为 “观察者” 安全访问 shared_ptr 管理的资源,避免悬空指针风险
  • 需配合 shared_ptr 使用,不能单独管理资源(无 RAII 特性)
  • 不能直接访问资源(无 operator*/operator-> 重载),仅作为 shared_ptr 的 “观察者”

weak_ptr 不能直接绑定到原始资源(如:new 分配的指针),只能绑定到 shared_ptr,且绑定后不影响 shared_ptr 的引用计数

shared_ptr<int> sp(new int(10));
  weak_ptr<int> wp(sp); // 绑定到 shared_ptr,sp 的计数仍为 1(不增加)

这一设计的关键作用是:

  • 打破 shared_ptr 的循环引用(如:双向链表节点互相引用时,用 weak_ptr 替代 shared_ptr 存储反向指针)
  • 观察 shared_ptr 管理的资源是否存活,又不干扰其释放逻辑

总结:weak_ptr 的使用原则

  • 定位shared_ptr 的 “辅助观察者”,不参与资源管理
  • 构造:只能绑定到 shared_ptr,不影响其引用计数
  • 访问:必须通过 lock()expired() 检查后访问,避免悬空指针
  • 场景:解决 shared_ptr 的循环引用问题,或安全观察 shared_ptr 管理的资源

二、模拟实现

/*-------------------------------------- weak_ptr 模拟实现(弱引用) --------------------------------------*/
template<class T>
  class weak_ptr
  {
  private:
  T* _ptr;  // 存储指向资源的原始指针,仅作为"观察者"
  public:
  //1.实现:“默认构造函数”
  weak_ptr()
  : _ptr(nullptr)  //1.初始化为空指针,不指向任何资源
  { }
  //2.实现:“拷贝构造函数” ---> 从shared_ptr构造:弱引用shared_ptr管理的资源
  weak_ptr(const shared_ptr<T>& sp)
    : _ptr(sp.get())  //1.通过shared_ptr的get()方法获取原始指针
    { }
    /* 说明:
    *    1. sp是被弱引用的“shared_ptr对象”
    *    2. 此处仅拷贝指针值,不修改sp的引用计数
    */
    //3.实现:“赋值运算符”---> 从shared_ptr弱引用资源(更新指向) 
    weak_ptr<T>& operator=(const shared_ptr<T>& sp)
      {
      //1.更新内部指针为“sp管理的资源”
      _ptr = sp.get();
      //2.返回自身引用以支持链式操作
      return *this;    // 同样不影响sp的引用计数
      }
      /* 说明:
      *    1. 参数:sp是被弱引用的“shared_ptr对象”
      *    2. 作用:将当前weak_ptr的指向更新为shared_ptr管理的资源
      */
      //4.实现:“获取原始指针” 
      T* get() const
      {
      return _ptr;  // 返回存储的原始指针
      }
      /* 说明:
      *    1. 注意:此实现为简化版本,标准库中需通过lock()方法转换为shared_ptr才能安全访问
      *    2. 风险:返回的指针可能已被释放(成为野指针),因为weak_ptr不跟踪资源是否存活
      */
      };

1. weak_ptr怎么检查资源是否存活?

资源访问的安全规则:由于 weak_ptr 不直接管理资源,访问资源前必须先检查资源是否存活


常用方式有两种:

(1)用 expired() 检查资源是否过期

  • expired() 返回 bool

    • true 表示 shared_ptr 已释放资源
    • false 表示资源存活
if (!wp.expired())
{
cout << "资源未释放" << endl;
}
else
{
cout << "资源已释放" << endl;
}

(2)用 lock() 安全获取资源(推荐)

  • lock()返回一个shared_ptr

    • 若资源存活:返回有效的 shared_ptr(计数 +1,保证访问安全)
    • 若资源已释放:返回空 shared_ptr(避免访问悬空指针)
shared_ptr<int> tmp = wp.lock();
  if (tmp)
  {
  // 资源存活,通过 tmp 访问(tmp 的计数临时 +1,确保访问时资源不释放)
  cout << *tmp << endl;
  }
  else
  {
  // 资源已释放,tmp 为空
  cout << "资源不可访问" << endl;
  }

------------ 循环引用 ------------

1. 什么是循环引用问题?

循环引用问题:是智能指针(尤其是 shared_ptr)在管理资源时,因对象间相互引用且无法打破依赖关系,导致资源无法释放的一种常见内存管理陷阱。


一、问题本质:引用计数的 “死锁”

  • 基础逻辑shared_ptr引用计数决定资源何时释放(计数归 0 时析构资源)
  • 循环引用:多个对象通过 shared_ptr 互相指向,形成 “环形依赖”,每个对象的引用计数都无法减到 0,最终资源无法释放,产生内存泄漏

二、场景还原:双向链表的循环引用

1. 创建节点与建立引用:

  • 此时引用关系:n1 引用 n2n2 引用 n1,形成环形依赖
struct Node
{
shared_ptr<Node> next; // 指向后继节点
  shared_ptr<Node> prev; // 指向前驱节点
    ~Node()
    {
    cout << "Node 析构" << endl;
    }
    };
    shared_ptr<Node> n1(new Node); // n1 计数 = 1
      shared_ptr<Node> n2(new Node); // n2 计数 = 1
        n1->next = n2; // n2 计数 +1 → 2
        n2->prev = n1; // n1 计数 +1 → 2

在这里插入图片描述

2. 进行析构时的 “死锁”:
n1n2 离开作用域,本应析构:

  • n1 析构 → 计数 2-1=1(因 n2->prev 仍引用它,无法释放)
  • n2 析构 → 计数 2-1=1(因 n1->next 仍引用它,无法释放)

最终n1n2 的计数始终为 1,资源永远无法释放,内存泄漏发生。

在这里插入图片描述


2. 怎么解决循环引用问题?

解决思路:用 weak_ptr 打破循环

weak_ptr:是 shared_ptr 的 “弱引用” 辅助工具,不参与引用计数,仅观察资源是否存活。

方法一:

修改双向链表的节点结构:

struct Node
{
weak_ptr<Node> next; // 弱引用,不影响计数
  weak_ptr<Node> prev; // 弱引用,不影响计数
    ~Node()
    {
    cout << "~Node()" << endl;
    }
    };

int main()
{
shared_ptr<Node> n1(new Node);
  shared_ptr<Node> n2(new Node);
    n1->_next = n2; // n2 计数仍为 1(weak_ptr 不增加计数)
    n2->_prev = n1; // n1 计数仍为 1(weak_ptr 不增加计数)
    // 离开作用域时:
    // n1 析构 → 计数 1-1=0 → 触发析构,next(weak_ptr)自动置空
    // n2 析构 → 计数 1-1=0 → 触发析构,prev(weak_ptr)自动置空
    }

当 n1、n2 建立引用时:

  • n1->next = n2n2 计数仍为 1weak_ptr 不增加计数)
  • n2->prev = n1n1 计数仍为 1weak_ptr 不增加计数)

当 n1、n2 进行析构时:

  • n1 计数 1-1=0 → 触发析构,_nextweak_ptr)自动置空
  • n2 计数 1-1=0 → 触发析构,_prevweak_ptr)自动置空

方法二:

修改双向链表的节点结构:

  • weak_ptr 替代 shared_ptr 存储反向指针:
struct Node
{
shared_ptr<Node> _next;
  weak_ptr<Node> _prev; // 用 weak_ptr 打破循环
    ~Node()
    {
    cout << "~Node()" << endl;
    }
    };

int main()
{
shared_ptr<Node> n1(new Node);
  shared_ptr<Node> n2(new Node);
    n1->_next = n2; // n2 计数 +1 → 2
    n2->_prev = n1; // n1 计数仍为 1(weak_ptr 不增加计数)
    // 离开作用域时:
    // n1 析构 → 计数 1-1=0 → 释放节点,_next 自动置空  
    // n2 析构 → 计数 2-1=1 → 因 _prev 是 weak_ptr,不影响计数,最终计数 1-1=0 → 释放节点  
    }

n1n2 建立引用时:

  • n1->next = n2n2 计数仍为 +1 → 2
  • n2->prev = n1n1 计数仍为 1weak_ptr 不增加计数)

n1n2 进行析构时:

  • n1 计数 1-1=0 → 触发析构,nextweak_ptr)自动置空
  • n2 计数 2-1=1 +1 → 因 _prev 是 weak_ptr,不影响计数,最终计数 1-1=0 → 释放节点

总结:

  • 循环引用问题:是 shared_ptr相互依赖导致计数无法归零的内存管理陷阱,常见于双向链表、对象互相持有 shared_ptr 的场景

  • 解决问题关键是用 weak_ptr 替代循环依赖中的 shared_ptr,切断计数 “死锁”,保障资源释放逻辑正常执行

理解这一问题,能避免智能指针使用中的隐性内存泄漏,提升代码健壮性。

2. weak_ptr指针怎么使用?

代码案例:循环引用问题的实际案例

#include <iostream>
  #include <memory>  // 包含 shared_ptr 和 weak_ptr 相关的头文件
    using namespace std;
    // 定义链表节点结构体
    struct ListNode
    {
    //1.定义一个存储链表节点数据内容的变量
    //2.定义一个指向当前节点的“下一个”链表节点的智能指针
    //3.定义一个指向当前节点的“前一个”链表节点的智能指针
    int _data;
    //shared_ptr<ListNode> _next;
      //shared_ptr<ListNode> _prev;
        /*--------------------------------- 方法一 ---------------------------------*/
        //weak_ptr<ListNode> _next;  
          //weak_ptr<ListNode> _prev; 
            /* 这里是注释说明如何修改来解决循环引用问题:
            *     1. 如果改成 weak_ptr,当 n1->_next = n2; 这样绑定 shared_ptr 时
            *     2. 不会增加 n2 的引用计数,不参与资源释放的管理,就不会形成循环引用了
            */
            /*--------------------------------- 方法二 ---------------------------------*/
            shared_ptr<ListNode> _next;
              weak_ptr<ListNode> _prev;
                //1.实现:“构造函数”
                ListNode()
                {
                cout << "ListNode()" << endl;
                }
                //2.实现:“析构函数”
                ~ListNode()
                {
                cout << "~ListNode()" << endl;
                }
                };
                int main()
                {
                /*----------------------- 演示shared_ptr循环引用导致内存泄漏的情况 -----------------------*/
                //1.创建两个 ListNode 对象,并用 shared_ptr 管理,n1 和 n2 分别指向这两个对象
                shared_ptr<ListNode> n1(new ListNode);
                  shared_ptr<ListNode> n2(new ListNode);
                    //2.输出 n1 和 n2 的引用计数
                    cout << n1.use_count() << endl;
                    cout << n2.use_count() << endl;
                    /*说明:
                    *   1. 此时各自的引用计数都是 1
                    *   2. 因为每个对象目前只有对应的 shared_ptr(n1 对应第一个节点,n2 对应第二个节点)在管理它们
                    */
                    //3.让 n1 的 _next 指向 n2 所管理的节点
                    n1->_next = n2;
                    /* 说明:
                    *    1. 此时 n2 的引用计数会增加到 2
                    *    2. 因为现在有 n2 和 n1->_next 这两个 shared_ptr 在管理第二个节点
                    */
                    //4.让 n2 的 _prev 指向 n1 所管理的节点
                    n2->_prev = n1;
                    /* 说明:
                    *    1. 此时 n1 的引用计数会增加到 2
                    *    2. 因为现在有 n1 和 n2->_prev 这两个 shared_ptr 在管理第一个节点
                    */
                    //5.再次输出 n1 和 n2 的引用计数,
                    cout << n1.use_count() << endl;
                    cout << n2.use_count() << endl;
                    /* 说明:
                    *    1. 此时 n1 的引用计数是 2,n2 的引用计数是 2
                    *    2. 这是因为形成了循环引用:n1 -> _next -> n2 -> _prev -> n1
                    */
                    /*  以下是关于 weak_ptr 的注释说明:
                    *      1. weak_ptr 不支持管理资源,不支持 RAII(资源获取即初始化)机制
                    *      2. weak_ptr 是专门用来绑定 shared_ptr 的
                    *      3. 绑定的时候不会增加 shared_ptr 的引用计数,常作为一些场景(如:解决循环引用)的辅助管理工具
                    */
                    // weak_ptr<ListNode> wp(new ListNode);     // 错误用法,weak_ptr 不能直接这样创建,它要绑定到已有的 shared_ptr 上
                      // weak_ptr<ListNode> wp(n1);               // 正确用法:这样 wp 就绑定到了 n1 所管理的对象上,且不会增加 n1 的引用计数
                        return 0;
                        }

在这里插入图片描述

:下面的这种也是可以的,类似于上面的方法二,这里就不再演示了。

weak_ptr<ListNode> _next;
  shared_ptr<ListNode> _prev;

------------ 线程安全(暂时了解) ------------

1. 引用计数的线程安全风险是什么以及怎么解决?

引用计数的线程安全风险:

shared_ptr引用计数对象存储在堆上,当多个线程同时操作 shared_ptr(如:拷贝、析构)时,会并发修改引用计数。

  • 若引用计数用普通 int* 实现,并发修改会导致数据竞争(计数结果错误、程序崩溃等)

    // 线程 1
    shared_ptr<Resource> sp1 = make_shared<Resource>();
      // 线程 2
      shared_ptr<Resource> sp2 = sp1; // 拷贝导致引用计数 +1  

标准库给出的解决方案:

C++ 标准库中,shared_ptr 的引用计数采用原子操作(或:加锁)实现线程安全。

  • 实际底层用 atomic<int>* 管理计数,保证 ++/-- 操作的原子性
  • 无需用户手动加锁,拷贝、析构 shared_ptr 时的计数修改是线程安全的

2. 指向对象的线程安全边界是什么以及怎么解决?

指向对象的线程安全边界:

shared_ptr 仅保证自身引用计数的线程安全,不保证指向对象的线程安全。

  • 若多个线程直接访问 sp 指向的对象,仍会引发数据竞争

  • 需由使用 shared_ptr 的外层代码负责对象的线程安全(如:加锁、原子操作)

    shared_ptr<Resource> sp = make_shared<Resource>();
      // 线程 1 修改对象
      sp->data = 100;
      // 线程 2 同时修改对象
      sp->data = 200;

手动实现时的线程安全问题:

若手动模拟 shared_ptr(如:用 int* 管理计数),多线程场景会崩溃或内存泄漏:

template <typename T>
  class MySharedPtr
  {
  T* _ptr;
  int* _pcount; // 普通 int*,非线程安全  
  public:
  // 拷贝构造时修改计数,多线程下可能出错  
  MySharedPtr(const MySharedPtr& other)
  : _ptr(other._ptr)
  , _pcount(other._pcount)
  {
  (*_pcount)++; // 并发修改风险  
  }
  };

解决方案:

  • atomic<int>*替代int*,保证计数操作原子性

    atomic<int>* _pcount;
  • 或手动加锁(如:mutex),保护计数修改


总结:shared_ptr 的线程安全边界

操作类型线程安全保障责任边界
引用计数的 修改拷贝析构标准库保证(原子操作 / 加锁)无需用户干预
指向对象的 访问/修改不保证由用户通过锁、原子操作等保证

简言之shared_ptr 管好自己的 “引用计数”,但管不了你用它指向的对象 —— 对象的线程安全需用户自己控制。

通过明确 shared_ptr 的线程安全边界,可避免因误解其功能导致的多线程 bug,合理搭配锁或原子操作保障整体线程安全。

3. shared_ptr在多线程环境下怎么保证线程安全?(实际操作)

#include <iostream>
  #include <memory>    // 智能指针头文件
    #include <thread>    // 多线程支持
      #include <mutex>     // 互斥锁
        using namespace std;
        // 定义结构体 AA
        struct AA
        {
        /*------------------ 成员变量 ------------------*/
        int _a1 = 0;  // 成员变量,初始化为 0
        int _a2 = 0;  // 成员变量,初始化为 0
        /*------------------ 成员函数 ------------------*/
        //1.实现:“析构函数”
        ~AA()
        {
        cout << "~AA()" << endl; //对象销毁时输出提示
        }
        };
        int main()
        {
        //1.创建 shared_ptr 管理 AA 对象
        shared_ptr<AA> p(new AA);
          //2.定义循环次数(100000 次)
          const size_t n = 100000;
          //3.创建互斥锁,用于保护共享资源(AA 对象的 _a1、_a2)
          mutex mtx;
          //4.定义线程函数(Lambda 表达式)
          auto func = [&]()  //这里用 [&] 引用捕获,捕获外部变量
          {
          for (size_t i = 0; i < n; ++i) // 循环 n 次,模拟多线程并发操作
          {
          //4.1:拷贝智能指针p:p → copy
          shared_ptr<AA> copy(p); //注:若自己模拟实现的 shared_ptr 引用计数非线程安全,多线程拷贝会导致计数错误
            //4.2:加锁保护对 AA 对象的修改(避免数据竞争)
            {
            //第一步:加锁,作用域结束自动解锁
            unique_lock<mutex> lk(mtx);
              //第二步:修改 AA 对象的成员变量(多线程安全:已加锁)
              copy->_a1++;
              copy->_a2++;
              }
              }
              };
              //5.创建并启动两个线程
              thread t1(func);
              thread t2(func);
              //6.等待线程结束(避免主线程提前退出)
              t1.join();
              t2.join();
              //7.输出 AA 对象的成员变量(预期值应为 2 * n)
              cout << p->_a1 << endl;
                cout << p->_a2 << endl;
                  //8.输出智能指针 p 的引用计数
                  cout << p.use_count() << endl;  //注:若自己模拟实现的 shared_ptr 引用计数非线程安全,结果可能错误
                  return 0;
                  }

在这里插入图片描述

片段一:auto func = & 为什么要使用引用捕获?

auto func = [&]()  //这里用 [&] 引用捕获,捕获外部变量
{
for (size_t i = 0; i < n; ++i) // 循环 n 次,模拟多线程并发操作
{
//4.1:拷贝智能指针p:p → copy
shared_ptr<AA> copy(p); //注:若自己模拟实现的 shared_ptr 引用计数非线程安全,多线程拷贝会导致计数错误
  //4.2:加锁保护对 AA 对象的修改(避免数据竞争)
  {
  //第一步:加锁,作用域结束自动解锁
  unique_lock<mutex> lk(mtx);
    //第二步:修改 AA 对象的成员变量(多线程安全:已加锁)
    copy->_a1++;
    copy->_a2++;
    }
    }
    };

上面的问题其实本质上是下面两个问题:

  1. 为什么要进行捕获?
  2. 为什么必须用引用捕获([&])?

那下面我们就来回答这两个问题。


问题1:为什么要进行捕获?

先看代码里实际用到的外部变量:

//1.创建 shared_ptr 管理 AA 对象
shared_ptr<AA> p(new AA);
  //2.定义循环次数(100000 次)
  const size_t n = 100000;
  //3.创建互斥锁,用于保护共享资源(AA 对象的 _a1、_a2)
  mutex mtx;
  • p:循环里要拷贝 shared_ptr<AA> copy(p),必须访问外部定义的 p
  • mtx:加锁时要用 unique_lock<mutex> lk(mtx),必须访问外部定义的 mtx
  • n:循环条件 for (size_t i = 0; i < n; ++i),要用到外部的 n

这些变量都在 main 函数作用域,lambda 里要访问,就得通过 “捕获” 把外部变量 “带进来”。


问题2:为什么必须用引用捕获([&])?

  • 变量的性质决定

    • pshared_ptr<AA>如果用值捕获 [p],会拷贝一个新的智能指针,但多线程里需要所有线程都操作同一个 p(保证引用计数同步),值捕获会导致每个线程有独立拷贝,逻辑直接乱套

    • mtxmutexmutex 禁止拷贝(拷贝构造函数 =delete),只能通过引用捕获(或指针),否则编译报错

    • nconst size_t值捕获也能用,但结合其他变量都用引用,统一写成 [&] 更简洁

  • 代码逻辑需要
    多线程要共享操作 p(智能指针)、mtx(锁),必须让线程里的 lambda 访问 “原始变量”,而不是拷贝。如果用值捕获:

    • mtx 拷贝会编译失败(mutex 不能拷贝)

    • p 拷贝会让每个线程的 copy(p) 操作独立的智能指针,引用计数无法全局同步,最终结果完全错误


总结:这里必须用引用捕获的核心原因

  • p 需要共享访问:多线程必须操作同一个智能指针,保证引用计数全局同步,值捕获会破坏逻辑
  • mtx 无法拷贝:mutex 的拷贝构造被删除,只能用引用(或指针)捕获
  • n 虽能值捕获,但统一用 [&] 更简洁:代码里变量都需要 “共享访问”,没必要拆分捕获方式

所以:这里的 [&] 不是随意写的,是根据变量的性质和代码逻辑强制要求的,换成其他捕获方式要么编译报错,要么逻辑错误 。

在这里插入图片描述

posted on 2025-10-31 19:22  blfbuaa  阅读(47)  评论(0)    收藏  举报