快速C++中的入门智能指针

news2024/11/20 22:38:36

✨前言✨

📘 博客主页:to Keep博客主页
🙆欢迎关注,👍点赞,📝留言评论
⏳首发时间:2024年6月4日
📨 博主码云地址:博主码云地址
📕参考书籍:《C++ Primer》《C++编程规范》
📢编程练习:牛客网+力扣网
**由于博主目前也是处于一个学习的状态,如有讲的不对的地方,请一定联系我予以改正!!!

C++中智能指针

  • 1 智能指针的引入
  • 2 内存泄露的概念
  • 3 智能指针的概念
    • 3.1 RALL特性(智能指针核心思想)
    • 3.2 重载operator*与重载operator->
    • 3.3 auto_ptr(已弃用了解即可)
  • 4 常见的三种智能指针
    • 4.1 unique_ptr
    • 4.2 shared_ptr
    • 4.3 循环引用的问题
    • 4.4 weak_ptr
  • 5 删除器(了解)

本文实现的全部代码

1 智能指针的引入

所谓的智能指针就是帮助我们解决内存中的管理问题,结合所学过的异常,我们可以来看一下这样的一个场景:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{

	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

我们来想一想这样的三个问题
1、如果p1这里new 抛异常会如何?
2、如果p2这里new 抛异常会如何?
3、如果div调用这里又会抛异常会如何?

其实p1这里抛异常不会有任何的影响,但是p2要是抛出了异常,就必须被捕获,所以就会跳出Func函数,就会导致用new申请的p1没有被释放,就会造成内存泄露!同理如果是div调用抛出的异常,那么就会导致p1与p2都没有释放,会导致内存泄漏!所以为了解决这样的一个问题,C++中就引入了智能指针的概念!

2 内存泄露的概念

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

简单来说就是我们申请的资源没有得到及时的释放,而失去了对这块资源的控制,从而造成了内存泄漏!长期的内存泄露就会导致服务器相应很慢!

3 智能指针的概念

在简单了解完了以上两个概念,下面我们在来介绍一下什么是智能指针!

3.1 RALL特性(智能指针核心思想)

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

看到这里,也许你还是很懵逼,这是啥?没关系,接下来,我们就以具体的例子来理解一下这个思想!在智能指针引入的代码基础上,我们来写一个智能指针的类!

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

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

对于Func中的资源申请可以改为如下:

void Func()
{
	SmartPtr<int> s1(new int);
	SmartPtr<int> s2(new int);
	/*int* p1 = new int;
	int* p2 = new int;*/
	cout << div() << endl;
	/*delete p1;
	delete p2;*/
}

此时运行结果如下:
在这里插入图片描述

我们可以发现,即使div会抛出异常,此时申请的资源也可以正常的释放了!也就是说利用这样的一个SmartPtr的类,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这样子做有两种好处:

  • 不需要显示的去释放资源
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

3.2 重载operator*与重载operator->

要想上述的类像指针一样就需要重载以上两个运算符,拥有像指针一样的行为

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

综上所述,智能指针必须满足以上两个要求,拥有RALL特性和重载了operator*与operator->两个运算符!

3.3 auto_ptr(已弃用了解即可)

其实在C++98中就提出了智能指针,auto_ptr就是当时提出来的,但是它是一个大坑,auto_ptr的实质就是将资源的管理权限进行转让,为什么这么说呢?我们来看一下这样的一段代码,库里面的智能指针包含在memory这个头文件中:
在这里插入图片描述
我们发现如果使用拷贝构造,此时就会把a1中资源的管理转交给a2,那么此时如果我们在对a1进行解引用等操作就是非法行为,所以库里面的这个智能指针要慎用(现在已经被禁用了),现在一般都是使用C++11中提出的那三种常见的智能指针!如果不像auto_ptr这样处理拷贝构造,我们能否直接进行浅拷贝呢?答案其实也是不可以的,因为这样子做就会导致一个对象指向的资源被析构两次(下图所示)!那么如何解决这种拷贝的问题呢?我们可以接着往下看!

在这里插入图片描述
自行模拟实现auto_ptr的简易版本,代码如下:

//模拟实现auto_ptr
	template<class T>
	class auto_ptr {
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		//拷贝构造  实现资源管理权的转移
		//a1(a2)
		auto_ptr(const auto_ptr<T>& p)
		{
			_ptr = p._ptr;
			p._ptr = nullptr;
		}

		//赋值拷贝
		auto_ptr<T>& operator=(auto_ptr<T>& p)
		{
			//自己没给自己赋值情况下才进行处理
			if (this != &p) {
				_ptr = p._ptr;
				p._ptr = nullptr;
				return *this;
			}
		}

		T operator*()
		{
			return *_ptr;
		}

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

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete" << endl;
				delete _ptr;
			}
		}
	private:
		T* _ptr;
	};

4 常见的三种智能指针

4.1 unique_ptr

unique_ptr就采用禁止拷贝构造以及赋值拷贝来解决以上所说的拷贝问题!具体的实现代码如下所示:


	//模拟实现unique_ptr
	template<class T>
	class unique_ptr {
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		//拷贝构造与赋值拷贝构造禁止使用!
		unique_ptr(const unique_ptr<T>& p) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& p) = delete;

		T operator*()
		{
			return *_ptr;
		}

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

		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete" << endl;
				delete _ptr;
			}
		}
	private:
		T* _ptr;
	};

4.2 shared_ptr

shared_ptr就采用了引用计数原理来解决拷贝问题,示意图如下所示:
在这里插入图片描述

也就是说,智能指针对象中有两个指针,一个是指向资源的指针,另一个就是指向计数的一个指针!注意:别看我示意图画的都是指向同一个计数,就认为我们使用的就是静态的成员变量来计数的!我们应该采用每一个对象中都会有一个初始计数,值为1,然后通过一系列的操作,改变指针的指向,从而使得指向同样的一块计数!
具体实现代码如下所示:

//模拟实现shared_ptr
//利用引用计数
template<class T>
class shared_ptr {
public:
	shared_ptr(T* ptr)
		:_ptr(ptr),
		_Count(new int(1))
	{}
	//拷贝构造与赋值拷贝构造禁止使用!
	//s1(s2)
	shared_ptr(const shared_ptr<T>& p)
	{
		_ptr = p._ptr;
		_Count = p._Count;
		(*_Count)++;
	}
	//s1 = s2
	shared_ptr<T>& operator=(shared_ptr<T>& p)
	{
		//只有不是指向同一块资源的才可以进行赋值操作,对计数进行改变
		if (_ptr != p._ptr)
		{
			//如果被赋值对象仅有它自己指向一块空间
			//那么此时就必须先对资源进行清理
			//在进行赋值
			destory();
			_ptr = p._ptr;
			_Count = p._Count;
			(*_Count)++;
		}
	}
	//计数,引用计数的个数
	int use_Count()
	{
		return *_Count;
	}
	T operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	~shared_ptr()
	{
		destory();
	}
private:
	void destory()
	{
		if (--(*_Count)==0)
		{
			cout << "delete" << endl;
			delete _ptr;
			delete _Count;
		}
	}
	T* _ptr;
	int* _Count;
};

4.3 循环引用的问题

shared_ptr在某种特定的情况下就会出现循环引用的问题!我们结合以下场景来理解这样的一个问题:

//循环引用例子
struct ListNode {
	int _val;
	test::shared_ptr<ListNode> _next;
	test::shared_ptr<ListNode> _prev;
	
	ListNode(int val)
		:_val(val),
		_next(nullptr),
		_prev(nullptr)
	{}
};
int main()
{
	test::shared_ptr<ListNode> a1(new ListNode(2));
	test::shared_ptr<ListNode> a2(new ListNode(3));
	
	a1->_next = a2;
	a2->_prev = a1;
	return 0;
}

先来说明一下,为什么这里的节点是使用智能指针,因为如果是节点类型,我们就不能将节点连接起来了,a2是智能指针是属于自定义类型,而我们的a1->_next就是一个指针,属于内置类型,所以会出错,不能对节点进行连接了!
在这里插入图片描述

其实当只有a1->_next = a2与a2->_prev = a1只有一个的时候不会出现任何问题的,当时当两句同时出现的时候就有问题了!为什么会这么说呢?下面我用图解的方式来说明:


以上就是实现了那两句代码之后的示意图,shared_ptr中有两个指针,一个是指向计数的,一个是指向对应资源的指针!下图就是造成循环引用的示意图了!
在这里插入图片描述

4.4 weak_ptr

weak_ptr就可以很好的解决循环引用的问题,其原理就是不对计数处理,只要处理指针的指向,也就是说weak_ptr并不是参与资源管理的智能指针,不具备RALL特性!模拟实现代码如下所示:

//模拟实现weak_ptr
template<class T>
class weak_ptr {
public:
	weak_ptr(T* ptr)
		:_ptr(ptr)
	{}

	weak_ptr(const shared_ptr<T>& p)
	{
		_ptr =p.getPtr();
	}

	//s1 = s2
	weak_ptr<T>& operator=(shared_ptr<T>& p)
	{
		_ptr = p.getPtr();
		return *this;
	}

	T operator*()
	{
		return *_ptr;
	}

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

private:
	T* _ptr;
};
//解决循环引用
struct ListNode {
	int _val;
	test::weak_ptr<ListNode> _next;
	test::weak_ptr<ListNode> _prev;

	ListNode(int val)
		:_val(val),
		_next(nullptr),
		_prev(nullptr)
	{}

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

int main()
{
	test::shared_ptr<ListNode> a1(new ListNode(2));
	test::shared_ptr<ListNode> a2(new ListNode(3));

	a1->_next = a2;
	a2->_prev = a1;
	return 0;
}

我们可以借助析构函数,来看一下是否进行了释放,包括上一小节介绍的shared_ptr也可以利用析构来查看节点是否进行了释放!

5 删除器(了解)

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题!这里我就简单的介绍一下就行了,就是利用包装器原理,用户在构造的时候,选择利用函数指针,仿函数,lambda表达式构造对象进行删除就可以了!本文的全部实现代码已经上传码云了!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据集划分】假如你有超百万条oracle数据库数据(成真版)

【数据集划分】假如你有接近百万条oracle数据库数据&#xff08;成真版&#xff09; 写在最前面小结 数据集划分原因注意事项 1. 留出法&#xff08;Hold-out Method&#xff09;原理算法复杂度代码示例Scikit-learn的train_test_split分布式计算框架&#xff08;如Apache Spar…

JVM学习-Jprofiler

JProfiler 基本概述 特点 使用方便&#xff0c;界面操作友好对被分析的应用影响小(提供模板)CPU&#xff0c;Tread&#xff0c;Memory分析功能尤其强大支持对jdbc,noSql,jsp,servlet,socket进行分析支持多种模式(离线、在线)的分析支持监控本地、远程JVM跨平台&#xff0c;拥…

MongoDB~索引使用与优化

Study by&#xff1a; https://docs.mongoing.com/indexeshttps://www.cnblogs.com/Neeo/articles/14325130.html#%E5%85%B6%E4%BB%96%E7%B4%A2%E5%BC%95 作用 如果你把数据库类比为一本书&#xff0c;那书的具体内容是数据&#xff0c;书的目录就是索引&#xff0c;所以索引…

C++第三方库【httplib】断点续传

什么是断点续传 上图是我们平时在浏览器下载文件的场景&#xff0c;下载的本质是数据的传输。当出现网络异常&#xff0c;浏览器异常&#xff0c;或者文件源的服务器异常&#xff0c;下载都可能会终止。而当异常解除后&#xff0c;重新下载文件&#xff0c;我们希望从上一次下载…

用例篇03

正交表 因素&#xff1a;存在的条件 水平&#xff1a;因素的取值 最简单的正交表&#xff1a;L4(2) 应用 allpairs 来实现正交表。 步骤&#xff1a; 1.根据需求找出因素和水平 2.将因素和水平写入到excel表格中&#xff08;表格不需要保存&#xff09;&#xff08;推荐用…

文本批量高效编辑器:一键在每行结尾添加分隔符,助力文本处理飞速提升!

在信息爆炸的时代&#xff0c;文本处理成为了一项不可或缺的技能。然而&#xff0c;面对大量的文本数据&#xff0c;如何高效地进行处理却成为了一项挑战。这时&#xff0c;一款高效、易用的文本批量编辑器就显得尤为重要。这个软件就是首助编辑高手 首先&#xff0c;打开首助…

fairseq框架使用记录

sh命令 cmd"fairseq-train data-bin/$data_dir--save-dir $save_dir--distributed-world-size $gpu_num -s $src_lang -t $tgt_lang--arch $arch--dropout $dropout--criterion $criterion --label-smoothing 0.1--task mmt_vqa--optimizer adam --adam-betas (0.9, 0.98…

高并发系统限流原理

短时间内巨大的访问流量&#xff0c;我们如何让系统在处理高并发的同时还能保证自身系统的稳定性&#xff1f;估计有人会说&#xff0c;增加机器就可以了&#xff0c;因为我的系统架构设计就是按照分布式思想进行架构设计的&#xff0c;所以可以只需要增加机器就可以解决问题了…

代码随想录算法训练营day41

题目&#xff1a;01背包理论基础、416. 分割等和子集 参考链接&#xff1a;代码随想录 动态规划&#xff1a;01背包理论基础 思路&#xff1a;01背包是所有背包问题的基础&#xff0c;第一次看到比较懵&#xff0c;完全不知道dp数据怎么设置。具体分析还是dp五部曲&#xff…

Vue3实战笔记(58)—从零开始掌握Vue3插槽机制,基础入门

文章目录 前言插槽基础入门总结 前言 不论是组件封装还是分析源码&#xff0c;实际开发中经常接触插槽&#xff0c;插槽是干什么用的呢&#xff1f;组件之间能够接收任意类型的 JavaScript 值作为 props&#xff0c;但组件要如何接收模板内容呢&#xff1f;在某些场景中&#…

openssl 常用命令demo

RSA Private Key的结构&#xff08;ASN.1&#xff09; RSAPrivateKey :: SEQUENCE { version Version, modulus INTEGER, -- n publicExponent INTEGER, -- e privateExponent INTEGER, -- d prime1 INTEGER, -- …

k8s学习--ConfigMap详细解释与应用

文章目录 一 什么是configmapConfigMap 的好处ConfigMap 的限制 二.创建ConfigMap的4种方式1.在命令行指定参数创建2.在命令行通过多个文件创建3.在命令行通过文件提供多个键值对创建4.YAML资源清单文件创建 三 configmap的两种使用方法1.通过环境变量的方式传递给pod2.通过vol…

vue3+typescript 使用Codemirror

安装 // npm npm install codemirror-editor-vue3 codemirror^5.65.12// ts版 还需安装&#xff1a; npm install types/codemirror全局注册 修改main.ts&#xff1a; import { createApp } from vueimport App from ./App.vueimport { InstallCodemirro } from "code…

文件编码概念

文件的读取 open()函数&#xff1a; 打开一个已存在的文件&#xff0c;或者创建一个新文件 open(name,mode,encoding) name:是要打开的目标文件名的字符串&#xff08;可以包含文件所在的具体路径&#xff09; mode:设置打开文件的模式&#xff08;访问模式&#xff09;&am…

LabVIEW步进电机的串口控制方法与实现

本文介绍了在LabVIEW环境中通过串口控制步进电机的方法&#xff0c;涵盖了基本的串口通信原理、硬件连接步骤、LabVIEW编程实现以及注意事项。通过这些方法&#xff0c;用户可以实现对步进电机的精确控制&#xff0c;适用于各种自动化和运动控制应用场景。 步进电机与串口通信…

【Linux】信号(一)

信号我们将从信号产生&#xff0c;信号的保存&#xff0c;信号处理分别进行讲解~ 至少大思路是这样。开始之前还要进行一些基础知识的铺垫。 目录 从生活中提炼一些结论&#xff1a;信号概念的一些储备&#xff1a;信号产生&#xff1a;一、kill指令&#xff1a;二、键盘组合键…

[数据集][目标检测]轮胎检测数据集VOC+YOLO格式439张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;439 标注数量(xml文件个数)&#xff1a;439 标注数量(txt文件个数)&#xff1a;439 标注类别…

面试官:核心线程数为零时,线程池会处理任务吗?

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一波电子书籍资料&#xff0c;包含《Effective Java中文版 第2版》《深入JAVA虚拟机》&#xff0c;《重构改善既有代码设计》&#xff0c;《MySQL高性能-第3版》&…

Redis篇 list类型在Redis中的命令操作

list在redis基本的命令 一.基本命令1.lpush和range2.lpushx rpushx3.lpop rpop4.lindex linsert llen5.lrem6.ltrim lset7.blpop brpop 一.基本命令 list在redis中相当于数组或者顺序表. 1.lpush和range 2.lpushx rpushx 3.lpop rpop 4.lindex linsert llen 如果要插入的列表中…

详解 Spark 核心编程之累加器

累加器是分布式共享只写变量 一、累加器功能 ​ 累加器可以用来把 Executor 端的变量信息聚合到 Driver 端。在 Driver 程序中定义的变量&#xff0c;在 Executor 端的每个 Task 都会得到这个变量的一份新的副本&#xff0c;每个 task 更新这些副本的值后&#xff0c;传回 Dri…