当前位置: 首页 > news >正文

《C++进阶之C++11》【智能指针】(下)

【智能指针】(下)目录

  • 前言:
  • ------------ 智能指针的原理 ------------
  • auto_ptr
    • 一、基本介绍
    • 二、模拟实现
  • unique_ptr
    • 一、基本介绍
    • 二、模拟实现
  • shared_ptr
    • 一、基本介绍
    • 二、模拟实现
      • 1. function<void(T*)> _del; 怎么理解?
      • 2. 引用计数int* _pcount;为什么用指针类型?
      • 3. 为什么使用if(_ptr!= sp._ptr)进行检测自赋值?
  • weak_ptr
    • 一、基本介绍
    • 二、模拟实现
      • 1. weak_ptr怎么检查资源是否存活?
  • ------------ 循环引用 ------------
    • 1. 什么是循环引用问题?
    • 2. 怎么解决循环引用问题?
      • 方法一:
      • 方法二:
    • 2. weak_ptr指针怎么使用?
  • ------------ 线程安全(暂时了解) ------------
    • 1. 引用计数的线程安全风险是什么以及怎么解决?
    • 2. 指向对象的线程安全边界是什么以及怎么解决?
    • 3. shared_ptr在多线程环境下怎么保证线程安全?(实际操作)
      • 片段一:auto func = [&]() 为什么要使用引用捕获?

在这里插入图片描述

往期《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)); // 计数 = 1shared_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 为普通 intshared_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); // 计数 = 1shared_ptr<Node> n2(new Node); // 计数 = 1n1->_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 计数 = 1n1->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 → 2n2->_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;  // 成员变量,初始化为 0int _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 → copyshared_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 → copyshared_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 虽能值捕获,但统一用 [&] 更简洁:代码里变量都需要 “共享访问”,没必要拆分捕获方式

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

在这里插入图片描述

http://www.dtcms.com/a/446395.html

相关文章:

  • 桐城网站定制软件开发的学校有哪些
  • 海南省工程建设定额网站简单网站首页
  • K230基础-特征检测
  • 宁波网站建设内容深圳网站开发建设服务公司
  • CTFHub 信息泄露通关笔记11:HG泄露(4种方法)
  • 网站采用什么方法建设wordpress获取文章信息
  • 上海网站建设公司招聘wordpress用手机写博客
  • 网站为什么要更新wordpress保护插件
  • Maixcam学习笔记-寻址色块和直线
  • 您正在 GUI 下运行 Fcitx,但是 fcitx-config-qt 未被找到。该软件包名称通常为 fcitx5-configtool。现在将打开配置目录
  • 速通web全栈开发
  • 网站建设虚拟服务器赣州新闻最新消息
  • 33.搜索旋转排序数组;153.寻找旋转排序数组中的最小值 4. 寻找两个正序数组的中位数
  • 4准则下,2可加模糊测度满足单调性和有界性约束。假设没有任何其他先验信息,基于Marichal熵最大的目标,求解莫比乌斯参数。
  • 果洛州wap网站建设公司wordpress用哪个国外空间
  • 【IMX6ULL驱动学习】INPUT子系统
  • 上海做网站建设公司代理注册公司流程和费用
  • spring6学习笔记
  • 资料代做网站网站建设维护合同
  • 【Linux】 开启关闭MediaMTX服务
  • 网站qq启动链接怎么做宁波seo网络推广定制多少钱
  • 下载站用什么cms公众号微网站建设
  • 南阳做网站 汉狮公司wordpress游客
  • 网站建设比较好的律所无锡专业网站营销
  • 建设网站要注意哪些成都网站关键词
  • 违规管理系统后端接口文档
  • 月票车本地数据API后端实现文档
  • Scrapy 中间件详解:自定义下载器与爬虫的 “拦截器”
  • jQuery Mobile 过渡
  • 网站外链优化抖音代运营协议书范本