【C++】一篇文章带你熟练掌握<智能指针>及其模拟实现

目录

一、引入

二、智能指针的使用及原理

1、RAII

2、智能指针的原理

3、auto_ptr

4、unique_ptr

5、shared_ptr

6、weak_ptr 


一、引入

我们先分析一下为什么需要智能指针?

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw invalid_argument("Division by zero condition!");
	}

	return (double)a / (double)b;
}

void Func()
{
	int* p = new int[10];
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
	delete p;
	cout << "delete[]"<<p << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

当正常使用时,可以正常释放(delete)空间

 这里会使用异常处理,当div函数抛异常时,就会直接跳到catch捕获的地方,进行处理。

在 new 和 delete 之间如果 div() 抛异常了,那么开辟的空间 p 就无法释放。最终会造成内存泄露。我们之前面对这样的问题,是对 div() 函数套一层异常判断,如果出现异常,先释放资源,再将异常抛出给外面的捕获处理【异常处理】。但是这样还是会有缺陷,如果有很多个资源申请呢?或者有连续的资源申请呢?比如资源1申请,如果出现异常,那么就直接跳出去,如果资源2异常那么就需要对这个操作套一层异常处理,需要先将资源1的资源释放了,然后再重新抛异常给外面。这样是不是每次申请资源时都需要套上异常处理呢?这样也太麻烦了吧!因此 C++ 为了解决这个问题,发明了智能指针

使用智能指针出了作用域可以正常调用析构函数

class SmartPtr
{
public:
	SmartPtr(int* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		delete[] _ptr;
		cout << "delete[] " << _ptr << endl;
	}
private:
	int* _ptr;
};

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw invalid_argument("Division by zero condition!");
	}

	return (double)a / (double)b;
}

void Func()
{
	int* p = new int[10];
	SmartPtr sp(p);

	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

二、智能指针的使用及原理

1、RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
template<class T>
class SmartPtr
{
public:
	// RAII
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		delete[] _ptr;
		cout << "delete[] " << _ptr << endl;
	}
private:
	T* _ptr;
};

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw invalid_argument("Division by zero condition!");
	}

	return (double)a / (double)b;
}

void Func()
{
	// RAII
    //这里再怎么抛异常都不会影响资源的释放,因为资源是被智能指针对象管理着
    //当对象销毁时,管理的资源肯定会释放的。
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<double> sp2(new double[10]);

	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;

2、智能指针的原理

上述的 SmartPtr 还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可 以通过->去访问所指空间中的内容,因此:AutoPtr 模板类中还得需要将 * 、-> 重载下,才可让其 像指针一样去使用。智能指针本质上也是一个指针,那指针之间也是可以赋值,拷贝的、值拷贝的。

重载这些运算符之前已经写过很多次,这里就不再赘述了~

template<class T>
class SmartPtr
{
public:
	// RAII
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "~SmartPtr()->" << _ptr << endl;

		delete _ptr;
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

3、auto_ptr

一般正常使用智能指针,就可以避免大多数的内存泄露问题,但不排除乱用的。智能指针的应用能帮助我们很好的处理因为异常出现而导致的内存泄露问题。不过初期的智能指针还存在着问题,比如拷贝问题。C++98时期就已经存在智能指针 auto_ptr.(C++ 98的 auto_ptr 被吐槽众多,被很多公司所禁用)

    // C++98
	// 管理权转移,最后一个拷贝对象管理资源,被拷贝对象都被置空
	// 很多公司都明确规定了,不要用这个
	template<class T>
	class auto_ptr
	{
	public:
		// RAII
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete->" << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}

		// ap2(ap1)
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};

C++98的 auto_ptr 智能指针,处理这种问题的原理是:管理权转移
什么叫管理权转移呢?就比如ap2(ap1),将ap1拷贝给ap2,也就是用ap1构造ap2。首先我们要明白,ap1是管理着一块资源的,ap2还没有实例化,没有管理资源。这里直接将ap1管理资源的权力转移给ap2。然后ap1就没有权力管理资源了也就是不需要管理资源了。不管理资源是如何做到的呢?直接将智能指针对象里面的指针置空即可 

【总结】

拷贝时,会把被拷贝对象的资源管理转移给拷贝对象。而被被拷贝对象就没有资源管理,直接置空

存在问题:管理权转移后,再次访问被拷贝对象

ap1这个智能指针已经没有资源可以管理了,里面的指针直接置空了,不能再访问了

指针之间的拷贝肯定是值拷贝,因为是内置类型,不存在深拷贝。那浅拷贝我们不写,编译器生成的拷贝构造就是浅拷贝,但是如果指针浅拷贝了,就会出现这样的问题:①指向同一块的空间被释放两次②有一块空间没有释放,内存泄露。 

这个版本被吐槽的很多,因为你将ap1对象管理资源的权转移后,ap1就悬空了。如果有人要再次访问ap1呢?这时就会出现问题。(里面的指针必须置空,不然就会释放两次。)

4、unique_ptr

unique_ptr的实现原理:简单粗暴的防拷贝,利用delete关键字,将函数定义为删除函数,不能使用。指针赋值的操作也被禁止。

    // C++11 
	template<class T>
	class unique_ptr
	{
	public:
		// RAII
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			cout << "delete->" << _ptr << endl;

			delete _ptr;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		// C++11
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

	private:
		// C++98
		// 1、只声明不实现
		// 2、限定为私有
		//unique_ptr(const unique_ptr<T>& up);
		//unique_ptr<T>& operator=(const unique_ptr<T>& up);
	private:
		T* _ptr;
	};

5、shared_ptr

但是如果就是存在智能指针间的拷贝,那该怎么办呢?unique_ptr肯定不能使用了。
所以C++11又提供了一个可以允许拷贝的智能指针,那就是 shared_ptr.

shared_ptr的原理:是通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源。 

实现原理:<引用计数>

🟢 shared_ptr 在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。

🟢 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。

🟢如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;

🟢 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

我们想要的计数器应该是要伴随着资源的申请而生成。当有一个资源生成被智能指针管理后,就会生成一个计数器,来计算该资源受管理的个数。当有两个资源时,就会有两个计数器,各计算各的,互不影响。所以这里的计数器应该是动态计数器
当有资源申请时并给对象管理时(也就是调用对象的构造函数)时,就会动态生成一个计数器。

 

 shared_ptr 除了支持智能指针间的拷贝,还支持指针间的赋值。那么赋值重载是如何实现的呢?
首先赋值是两个都已经存在的对象进行赋值。而拷贝构造是已存在的拷贝给不存在的对象。假设两个存在的对象,各自都管理着一块资源,当两个对象进行赋值

  • 如果将sp3 赋值给 sp1,那么sp1 对象的指针和计数器指针都要指向sp3所指向的资源和计数器。
  • sp1对象管理的资源少了一个,所以计算器 -1;
  • sp3对象管理的资源多了一个,所以计算器 +1;

  • 同时还必须考虑到赋值对象的计数器减减后,需要进行判断,是否为0.如果计数器为0了,就说明该资源上没有对象管理,那么该资源就可以释放了。如下方的sp3 = sp1

  • 如果自己赋值给自己,可能会导致计数器 -1  为0,释放掉指向的资源,我们为了避免这种类型的发生,如果有自己赋值给自己的情况,直接返回 *this 即可
template<class T>
	class shared_ptr
	{
	public:
		// RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				//cout << "delete->" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}

		~shared_ptr()
		{
			release();
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);
		}

		// sp1 = sp3
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;

				++(*_pcount);
			}

			return *this;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
	};

6、weak_ptr 

shared_ptr几乎已经完美了,既具有智能指针的特性,又允许拷贝和赋值。但还是具有缺点的
该缺点就是:循环计数。当出现循环计数时,shared_ptr是没有办法解决。

什么叫循环计数问题呢?我们用一个链表来演示

struct ListNode
 {
   int _data;
   shared_ptr<ListNode> _prev;
   shared_ptr<ListNode> _next;
   ~ListNode(){ cout << "~ListNode()" << endl; }
 };
 int main()
 {
   shared_ptr<ListNode> node1(new ListNode);
   shared_ptr<ListNode> node2(new ListNode);
   cout << node1.use_count() << endl;
   cout << node2.use_count() << endl;
   node1->_next = node2;
   node2->_prev = node1;
   cout << node1.use_count() << endl;
   cout << node2.use_count() << endl;
   return 0;
}

循环引用分析:

  •  node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动 delete。
  •  node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  •  node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  •  也就是说_next析构了,node2就释放了。
  •  也就是说_prev析构了,node1就释放了。
  •  但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev 属于node2成员,所以这就叫循环引用,谁也不会释放。

首先我们需要明白引起循环计数的原因是什么,要理解什么场景下会发生循环计数。

  • 因为智能指针定义在节点的内部。
  • 因为计数器要等于1时才可以释放资源。

就是因为内部的智能指针参与了资源的管理,导致计数器增加,外面管理的资源的对象销毁后资源也无法销毁,需要里面的智能指针对象销毁才可以销毁,而里面的对象销毁又需要资源先销毁才可以销毁。所以最主要原因就是内部的智能指针管理资源。
所以 weak_ptr 实现的原理就是让节点里面的智能指针不管理资源,就单纯的链接节点,不参与资源的管理,但可以访问资源。
这样计数器就不会增加,当外面的对象销毁,资源就会正常销毁。

【解决方案】在引用计数的场景下,把节点中的_prev和_next 改成 weak_ptr 就可以了

【解决原理】node1->_next = node2; 和node2->_prev = node1;时weak_ptr的_next和 _prev不会增加node1和node2的引用计数。 

template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/593690.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Day30:热帖排行、生成长图、将文件上传到云服务器、优化热门帖子列表、压力测试

热帖排行 不同的算分方式&#xff1a; 只存变化的帖子到redis中&#xff0c;每五分钟算一次分&#xff0c;定时任务 存redis 构建redis键 //统计帖子分数 //key:post:score -> value:postId public static String getPostScoreKey() {return PREFIX_POST SPLIT "…

【解决】docker一键部署报错

项目场景见&#xff1a;【记录】Springboot项目集成docker实现一键部署-CSDN博客 问题&#xff1a; 1.docker images 有tag为none的镜像存在。 2.有同事反馈&#xff0c;第一次启动docker-compose up -d 项目无法正常启动。后续正常。 原因&#xff1a; 1.服务中指定了镜像m…

mqtt上行数据传送

{"id": "123","version": "1.0","params": {"wendu": {"value": 25.0},"humi": {"value": 23.6}} } 不要time!!!!!!!!!!!!!!!!!!!!!!!!!!! 下面是官方文档的代码&#xff0c;我用…

自制RAG工具:docx文档读取工具

自制RAG工具&#xff1a;docx文档读取工具 1. 介绍2. 源码2.1 chunk2.2 DocReader 3. 使用方法3.1 文档格式设置3.2 代码使用方法 1. 介绍 在RAG相关的工作中&#xff0c;经常会用到读取docx文档的功能&#xff0c;为了更好地管理文档中的各个分块&#xff0c;以提供更高质量的…

伺服电机初识

目录 一、伺服电机的介绍二、伺服电机的基本原理三、伺服电机的技术特点四、伺服电机的分类五、实际产品介绍1、基本技术规格&#xff1a;2、MD42电机硬件接口3、通讯协议介绍3.1 通讯控制速度运行3.2 通讯控制位置运行3.3 通讯控制转矩运行 4、状态灯与报警信息 一、伺服电机的…

MyScaleDB:SQL+向量驱动大模型和大数据新范式

大模型和 AI 数据库双剑合璧&#xff0c;成为大模型降本增效&#xff0c;大数据真正智能的制胜法宝。 大模型&#xff08;LLM&#xff09;的浪潮已经涌动一年多了&#xff0c;尤其是以 GPT-4、Gemini-1.5、Claude-3 等为代表的模型你方唱罢我登场&#xff0c;成为当之无愧的风口…

富文本编辑器CKEditor4简单使用-07(处理浏览器不支持通过工具栏粘贴问题 和 首行缩进的问题)

富文本编辑器CKEditor4简单使用-07&#xff08;处理浏览器不支持通过工具栏粘贴问题 和 首行缩进的问题&#xff09; 1. 前言——CKEditor4快速入门2. 默认情况下的粘贴2.1 先看控制粘贴的3个按钮2.1.1 工具栏粘贴按钮2.1.2 存在的问题 2.2 不解决按钮问题的情况下2.2.1 使用ct…

Linux——基础IO2

引入 之前在Linux——基础IO(1)中我们讲的都是(进程打开的文件)被打开的文件 那些未被打开的文件呢&#xff1f; 大部分的文件都是没有被打开的文件&#xff0c;这些文件在哪保存&#xff1f;磁盘(SSD) OS要不要管理磁盘上的文件&#xff1f;(如何让OS快速定位一个文件) 要…

设计模式之拦截过滤器模式

想象一下&#xff0c;在你的Java应用里&#xff0c;每个请求就像一场冒险旅程&#xff0c;途中需要经过层层安检和特殊处理。这时候&#xff0c;拦截过滤器模式就化身为你最可靠的特工团队&#xff0c;悄无声息地为每一个请求保驾护航&#xff0c;确保它们安全、高效地到达目的…

Endnote X9 20 21如何把中文引用的et al 换(变)成 等

描述 随着毕业的临近&#xff0c;我在写论文时可能会遇到在引用的中文参考文献中出现“et al”字样。有的学校事比较多&#xff0c;非让改成等等&#xff0c;这就麻烦了。 本身人家endnote都是老美的软件&#xff0c;人家本身就是针对英文文献&#xff0c;你现在让改成等等&a…

JavaScript的操作符运算符

前言&#xff1a; JavaScript的运算符与C/C一致 算数运算符&#xff1a; 算数运算符说明加-减*乘%除/取余 递增递减运算符&#xff1a; 运算符说明递增1-- 递减1 补充&#xff1a; 令a1&#xff0c;b1 运算a b ab12ab22ab--10a--b00 比较(关系)运算符&#xff1a; 运算…

【ChatGPT with Date】使用 ChatGPT 时显示消息时间的插件

文章目录 1. 介绍2. 使用方法2.1 安装 Tampermonkey2.2 安装脚本2.3 使用 3. 配置3.1 时间格式3.2 时间位置3.3 高级配置(1) 生命周期钩子函数(2) 示例 4. 反馈5. 未来计划6. 开源协议7. 供给开发者自定义修改脚本的文档7.1 项目组织架构7.2 定义新的 Component(1) 定义一个新的…

提示找不到msvcr110.dll怎么办,分享多种靠谱的解决方法

当用户在操作计算机时遇到系统提示“找不到msvcr110.dll&#xff0c;无法继续执行代码”这一错误信息&#xff0c;这个问题会导致软件无法启动运行。本文将介绍计算机找不到msvcr110.dll的5种详细的解决方法&#xff0c;帮助读者解决这个问题。 一&#xff0c;关于msvcr110.dll…

《十六》QT TCP协议工作原理和实战

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍如何运用QTcpSocket组件实现基于TCP的网络通信…

论文| Where Is Your Place, Visual Place Recognition?

论文| Where Is Your Place, Visual Place Recognition&#xff1f;

1.pytorch加载收数据(B站小土堆)

数据的加载主要有两个函数&#xff1a; 1.dataset整体收集数据&#xff1a;提供一种方法去获取数据及其label&#xff0c;告诉我们一共有多少数据&#xff08;就是自开始把要的数据和标签都收进来&#xff09; 2.dataloader&#xff0c;后面传入模型时候&#xff0c;每次录入数…

某站戴师兄——Excel学习笔记

1、拿到源数据第一件事——备份工作表&#xff0c;隐藏 Ctrlshift键L打开筛选 UV (Unique visitor)去重 是指通过互联网访问、浏览这个网页的自然人。访问网站的一台电脑客户端为一个访客。00:00-24:00内相同的客户端只被计算一次。一天内同个访客多次访问仅计算一个UV。 PV …

【C++】详解STL的容器之一:list

目录 简介 初识list 模型 list容器的优缺点 list的迭代器 常用接口介绍 获取迭代器 begin end empty size front back insert push_front pop_front push_back pop_back clear 源代码思路 节点设计 迭代器的设计 list的设计 begin() end() 空构造 ins…

【编程题-错题集】chika 和蜜柑(排序 / topK)

牛客对于题目链接&#xff1a;chika和蜜柑 (nowcoder.com) 一、分析题目 排序 &#xff1a;将每个橘⼦按照甜度由高到低排序&#xff0c;相同甜度的橘子按照酸度由低到高排序&#xff0c; 然后提取排序后的前 k 个橘子就好了。 二、代码 1、看题解之前AC的代码 #include <…

企业计算机服务器中了halo勒索病毒怎么处理,halo勒索病毒解密流程

随着网络技术的不断发展&#xff0c;网络在企业生产运营过程中发挥着重大作用&#xff0c;很多企业利用网络开展各项工作业务&#xff0c;网络也大大提高了企业的生产效率&#xff0c;但随之而来的网络数据安全问题成为众多企业关心的主要话题。近日&#xff0c;云天数据恢复中…
最新文章