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

【智能指针】(下)目录

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

使用示例

cpp 复制代码
auto_ptr<int> p1(new int(10));

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

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

二、模拟实现

cpp 复制代码
/*-------------------------------------- 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),转移所有权后原指针变为空

使用示例

cpp 复制代码
unique_ptr<int> p1(new int(10));

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

二、模拟实现

cpp 复制代码
/*-------------------------------------- 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 时,自动释放资源

使用示例

cpp 复制代码
shared_ptr<int> p1(new int(10)); // 计数 = 1

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

二、模拟实现

cpp 复制代码
/*-------------------------------------- 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; 怎么理解?

cpp 复制代码
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* 类型的单个对象

    cpp 复制代码
    _del([](T* ptr) { delete ptr; })

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

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

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

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

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

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

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

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

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

cpp 复制代码
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 会有自己的计数副本

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

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

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

    cpp 复制代码
    // 正确设计:_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)进行检测自赋值?

cpp 复制代码
//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),但无法检测 "不同对象但共享同一资源" 的情况。

cpp 复制代码
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),直接执行赋值逻辑,会发生以下错误:

    cpp 复制代码
    // 假设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 形成的循环引用,让引用计数逻辑回归正常

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

cpp 复制代码
#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 配合)

cpp 复制代码
#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 的引用计数

cpp 复制代码
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 管理的资源

二、模拟实现

cpp 复制代码
/*-------------------------------------- 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 表示资源存活
cpp 复制代码
if (!wp.expired()) 
{
    cout << "资源未释放" << endl;
} 

else 
{
    cout << "资源已释放" << endl;
}

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

  • lock()返回一个shared_ptr

    • 若资源存活 :返回有效的 shared_ptr(计数 +1,保证访问安全)
    • 若资源已释放 :返回空 shared_ptr(避免访问悬空指针)
cpp 复制代码
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,形成环形依赖
cpp 复制代码
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 的 "弱引用" 辅助工具,不参与引用计数,仅观察资源是否存活。

方法一:

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

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

cpp 复制代码
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 存储反向指针:
cpp 复制代码
struct Node 
{
	shared_ptr<Node> _next;
	weak_ptr<Node> _prev; // 用 weak_ptr 打破循环

	~Node() 
	{
		cout << "~Node()" << endl; 
	}
};

cpp 复制代码
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指针怎么使用?

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

cpp 复制代码
#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;
}

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

cpp 复制代码
weak_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;

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

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

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

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

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

    cpp 复制代码
    // 线程 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 的外层代码负责对象的线程安全(如:加锁、原子操作)

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

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

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

cpp 复制代码
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*,保证计数操作原子性

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


总结:shared_ptr 的线程安全边界

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

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

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

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

cpp 复制代码
#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 = & 为什么要使用引用捕获?

cpp 复制代码
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:为什么要进行捕获?

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

cpp 复制代码
//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(保证引用计数同步),值捕获会导致每个线程有独立拷贝,逻辑直接乱套

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

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

  • 代码逻辑需要

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

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

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


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

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

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

相关推荐
Mingze03142 小时前
考研408之栈与队列学习
开发语言·c++·学习·考研·算法
青草地溪水旁2 小时前
第五章:原型模式 - 克隆大法的大师
c++·设计模式·原型模式
丰锋ff2 小时前
2024 年真题配套词汇单词笔记(考研真相)
笔记·考研
从前慢,现在也慢2 小时前
【STL学习】(9)priority_queue
c++·学习
序属秋秋秋2 小时前
《C++进阶之C++11》【智能指针】(上)
c++·笔记·学习·面试·c++11·智能指针·新特性
guigu20123 小时前
C++ STL 深度解析:容器、迭代器与算法的协同作战
c++·嵌入式编程
Yupureki3 小时前
从零开始的C++学习生活 3:类和对象(中)
c语言·c++·学习·visual studio
十安_数学好题速析3 小时前
根式方程:结构联想巧用三角代换
笔记·学习·高考
励志不掉头发的内向程序员3 小时前
【Linux系列】并发世界的基石:透彻理解 Linux 进程 — 进程状态
linux·运维·服务器·开发语言·学习