《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)
此时
sp1
和sp2
的计数是独立的,无法同步更新,导致引用计数失去意义。若 _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_ptr
用if (this != &ap)
检测自赋值的逻辑有本质区别。
先明确:什么是 “自赋值”?
自赋值不仅指 “自己给自己赋值”(如:
sp = sp
)还包括 “两个指向同一资源的 shared_ptr 互相赋值”(如:
sp1 = sp2
,且sp1
和sp2
原本就共享同一资源)这两种情况都属于 “自赋值场景”,需要通过检测避免错误。
为什么不用 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_ptr
是shared_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
引用n2
,n2
引用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. 进行析构时的 “死锁”:
当n1
和n2
离开作用域,本应析构:
n1
析构 → 计数2-1=1
(因n2->prev
仍引用它,无法释放)n2
析构 → 计数2-1=1
(因n1->next
仍引用它,无法释放)最终:
n1
和n2
的计数始终为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 = n2
→n2
计数仍为1
(weak_ptr
不增加计数)n2->prev = n1
→n1
计数仍为1
(weak_ptr
不增加计数)当 n1、n2
进行析构
时:
n1
计数1-1=0
→ 触发析构,_next
(weak_ptr
)自动置空n2
计数1-1=0
→ 触发析构,_prev
(weak_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 → 释放节点
}
当
n1
、n2
建立引用时:
n1->next = n2
→n2
计数仍为 +1 →2
n2->prev = n1
→n1
计数仍为1
(weak_ptr
不增加计数)当
n1
、n2
进行析构时:
n1
计数1-1=0
→ 触发析构,next
(weak_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:为什么要进行捕获?
先看代码里实际用到的外部变量:
//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:为什么必须用引用捕获([&])?
变量的性质决定:
p
是shared_ptr<AA>
,如果用值捕获 [p],会拷贝一个新的智能指针,但多线程里需要所有线程都操作同一个 p(保证引用计数同步),值捕获会导致每个线程有独立拷贝,逻辑直接乱套
mtx
是mutex
, mutex 禁止拷贝(拷贝构造函数 =delete),只能通过引用捕获(或指针),否则编译报错
n
是const size_t
,值捕获也能用,但结合其他变量都用引用,统一写成 [&] 更简洁代码逻辑需要:
多线程要共享操作p
(智能指针)、mtx
(锁),必须让线程里的 lambda 访问 “原始变量”,而不是拷贝。如果用值捕获:
mtx
拷贝会编译失败(mutex 不能拷贝)
p
拷贝会让每个线程的copy(p)
操作独立的智能指针,引用计数无法全局同步,最终结果完全错误
总结:这里必须用引用捕获的核心原因
- p 需要共享访问:多线程必须操作同一个智能指针,保证引用计数全局同步,值捕获会破坏逻辑
- mtx 无法拷贝:mutex 的拷贝构造被删除,只能用引用(或指针)捕获
- n 虽能值捕获,但统一用 [&] 更简洁:代码里变量都需要 “共享访问”,没必要拆分捕获方式
所以:这里的
[&]
不是随意写的,是根据变量的性质和代码逻辑强制要求的,换成其他捕获方式要么编译报错,要么逻辑错误 。