冰冰学习笔记:智能指针

news2024/11/18 19:38:51

欢迎各位大佬光临本文章!!!

 

还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog

我的gitee:冰冰棒 (bingbingsupercool) - Gitee.comhttps://gitee.com/bingbingsurercool


系列文章推荐

冰冰学习笔记:《异常处理》

冰冰学习笔记:《C++11的新特性》


目录

系列文章推荐

前言

1.为什么需要只能指针

2.什么是智能指针

3.智能指针怎么用

3.1auto_ptr

3.2unique_ptr

3.3shared_ptr

3.4weak_ptr

4.模拟实现智能指针


前言

        前面的章节中我们学习了异常的使用方法,当程序抛出异常时,程序会直接跳转到最近的捕获区域进行异常的处理,这种处理方式保证了程序的不崩溃,只针对一个区域内的某种错误进行处理。这样的代码看似很正常,但往往会忽略掉某些内存的处理。例如我们在某个函数调用前向内存申请了部分空间,本来内存的释放逻辑是在函数调用之后进行完成,但是如果函数内部出现异常错误,函数将直接跳转,申请的内存无法释放,就造成了内存泄漏的风险。这种代码我们怎么处理呢?

1.为什么需要只能指针

首先我们先看一段代码:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	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;
}

        如代码所示,当new p1出现问题时,代码会抛出异常,空间无法开辟成功。当new p2出现错误时意味着p1已经被开辟,但是程序不会走到delete p1处,p1无法被释放,照成内存泄漏的风险。当调用函数出现异常,则p1,p2都无法进行内存的释放,依然存在内存泄漏的风险。

        在当前的运行环境中,函数调用完毕后进程也就结束了,即便我们没有释放的内存也会在进程结束时一起归还给操作系统,因此这种内存泄漏并没有什么大的风险。 但是,服务器上的程序是一旦开机几乎不会停止的进程,如果我们的内存一直在泄漏,那么我们的机器将会越来越慢,最终因为内存不够而导致进程崩溃。

        内存泄漏究竟是什么呢?内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。因此我们只是失去了对该内存的控制,并非内存的丢失。所以内存泄漏实际上是指针的丢失而并非内存的丢失。内存泄漏带来的危害是很可怕的,往往会照成操作系统,后台服务的崩溃。

        照成内存泄漏的原因有可能是我们对申请的资源忘记释放,也有可能因为异常的跳转导致本该释放的内存没有办法释放。在C/C++程序中,我们往往关注两种方面的内存泄漏:

(1)堆内存泄露(Heap leak)

        堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

(2)系统资源泄漏

        指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放 掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

        内存泄漏非常的常见,我们通常采用两种方式避免内存泄漏,事前预防(智能指针)、事后检测(泄露工具)。

        对于事前预防,我们需要对自己使用的内存进行即使的释放,养成良好的代码习惯,当遇到异常时,使用智能指针进行预防。事后检测的程序有很多,但有相当多的工具不靠谱。

因此我们发现智能指针就是来解决内存泄漏的问题的。

2.什么是智能指针

        智能指针即RAII(Resource Acquisition Is Initialization,资源获得立即初始化)是一种利用对象生命周期来控制程序资源的简单技术。如最开始提出的例子,如果我们的指针能够像对象一样,出了作用域自己释放销毁,那么就不会出现内存泄漏的情况了。基于这一思想,智能指针孕育而生了。我们将指针托管给某个对象,在对象构造时获取资源,在对象析构时释放资源。这样我们的资源不用再手动进行释放,对象所需要的资源在生命周期内始终有效。

例如我们可以实现下面的类似代码:

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* pt)
		:_ptr(pt)
	{}
	~SmartPtr()
	{
        cout<<"delete "<<endl;
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};

        使用我们提供的类进行指针的托管,在开头提供的例子中,即便函数出现除0错误,指针的资源依旧能够得到释放,不会照成内存泄漏的风险。

        但是上面的SmartPtr并不能称作智能指针,因为该类型的对象不能像正常的指针一样进行解引用操作,也不能使用箭头操作符。

接下来我们对其进行运算符重载:

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* pt) :_ptr(pt) {}
	~SmartPtr()
	{
		cout << "delete " << endl;
		if (_ptr)
			delete _ptr;
	}
	T& operator*() { return *(_ptr) ;}
	T* operator->() { return _ptr ;}
private:
	T* _ptr;
};

这样实现的智能指针能够像指针一样进行解引用操作,但是不能进行拷贝指针的操作。

        对于拷贝出错的原因我们进行排查会发现进行拷贝后,两个指针指向了同一块区域,在对象进行销毁时,指针会释放两次,因此进行报错。 这就是典型的浅拷贝问题,但是我们又不能实现深拷贝的形式,因为普通的指针就是浅拷贝,两个指针指向的就是同一块区域。

那我们应该如何进行智能指针的设计呢?我们先看看库中的实现方式。

3.智能指针怎么用

        智能指针并不是我们实现的这么简单,C++库中为我们提供了智能指针。C++98中库中提供了auto_ptr的智能指针。

3.1auto_ptr

        在接口函数中,提供了能够显示释放的接口函数release和获得指针的get函数。auto_ptr可以支持拷贝,他是基于管理权进行转移的拷贝方式。

        这是一种极不负责任的做法,auto_ptr将p1的指针直接转移给了p2,虽然不会造成析构两次的错误,但是如果我们想使用p1时就会出现野指针的问题。 总之来说,auto_ptr是一个极其失败的设计,尽量不要使用。

3.2unique_ptr

        C++11中吸收了boost库的智能指针scope_ptr,设计了unique_ptr。unique_ptr的做法就是简单粗暴的防拷贝

3.3shared_ptr

        但是我们有时候确实需要指针的拷贝,那么该怎么办呢?C++11中还提供了可以进行拷贝的智能指针shared_ptr。

        shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。每一次进行指针的拷贝,计数就会进行++操作,释放一次指针,计数就会进行--操作,直到最后一个指针被释放,计数变为0,调用delete进行空间的释放。

        其中接口use_count是返回该空间有几个指针指向,unique则表示该指针是否唯一的指向一块空间。 

3.4weak_ptr

        weak_ptr是为了解决shared_ptr的循环引用问题而专门设计的一个指针,weak_ptr不会增加引用计数,只是进行指针的拷贝。weak_ptr不进行资源的管理。

        什么是循环引用问题呢?当我们进行链表节点的搭建时,如果使用shared_ptr进行指针的管理就会出现下面的状况。

但是我们这样进行了管理,会发现指针无法进行释放了! 

        这就是典型的循环引用问题,在节点不连接的时候,n1的计数为1,n2的计数为1,当进程结束后,引用计数减为0,调用析构函数进行资源的释放。但是当n1的next指向n2时,n2节点就会有两个指针指向,一个是n2,一个是n1中的next。n2的prev类似,节点链接后,n1节点也有两个指针指向。此时进程结束,n1,n2被释放,计数减1变为1,next指向n2,prev指向n1。资源不会被清理,只有当next释放n2才会释放,n2释放prev才会释放,prev释放n1才会释放,n1释放next才会释放,此时形成了一个闭环。

        为了解决这一问题,我们需要使用weak_ptr来进行管理,weak_ptr不会增加计数,因此进程结束,节点依旧是从1减到0然后释放。

4.模拟实现智能指针

接下来我们进行智能指针的模拟实现:

auto_ptr的实现:

我们要注意在赋值操作时我们需要释放掉原先的空间,避免内存空间泄漏。

	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr) :_ptr(ptr) {}
		auto_ptr(auto_ptr<T> &p) :_ptr(p._ptr)
		{
			p._ptr = nullptr;
		}
		~auto_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
			}
		}
		auto_ptr<T>& operator=(auto_ptr<T>& p)
		{
			if (this != &p)
			{
				if (_ptr)
				{
					delete _ptr;
				}
				_ptr = p._ptr;
				p._ptr = nullptr;
			}
		}
		T& operator*(){ return *_ptr;}
		T* operator->(){ return _ptr;}
	private:
		T* _ptr = nullptr;
	};

unique_ptr中不能进行拷贝和赋值,所以我们直接使用delete删除掉拷贝和赋值函数。

	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr) :_ptr(ptr) {}
		~unique_ptr() { delete _ptr; }
		unique_ptr(unique_ptr<T>& p) = delete;
		unique_ptr<T> operator=(unique_ptr<T>& p) = delete;
		T& operator*() { return *_ptr; }
		T* operator->(){return _ptr; }
	private:
		T* _ptr = nullptr;
	};

        shared_ptr需要使用引用计数来进行拷贝和赋值,我们不能使用静态成员变量进行计数,因为静态成员变量在一个类中只有一份,不同类型的指针都会增加这个静态成员。所以我们采用初始化时多开辟空间存储计数的原理进行实现。

	template<class T>
	class shared_ptr
	{
	public:
		T& operator*() { return *_ptr; }
		T* operator->() { return _ptr; }
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _n(new int(1))
		{}
		shared_ptr(shared_ptr<T>& p)
			:_ptr(p._ptr)
			, _n(p._n)
		{
			(*_n)++;
		}
		void release()
		{
			if (--(*_n) == 0)
			{
				delete _ptr;
				delete _n;
			}
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& p)
		{
			if (_ptr == p._ptr)
				return *this;
			release();
			_ptr = p._ptr;
			_n = p._n;
			(*_n)++;
			return *this;
		}
		~shared_ptr()
		{
			release();
		}
		bool unique()
		{
			if ((*_n) == 1)
				return true;
			else
				return false;
		}
		int use_count()
		{
			return _n;
		}
		T* get()const
		{
			return _ptr;
		}
	private:
		int* _n;
		T* _ptr=nullptr;
	};

weak_ptr最重要的是需要进行shared_ptr的构造:

	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.coloradmin.cn/o/163686.html

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

相关文章

vite 在proxy代理中更改headers

vite 在proxy代理中更改headers 平时我们在对接接口时&#xff0c;我们都是配置代理解决跨域问题 proxy: {^/api: {target: envConfig.VITE_APP_BASE_URL,changeOrigin: true,rewrite: (path) > path.replace(/^/api/, ),}} 某天你明明配置好了代理&#xff0c;浏览器还是会…

嵌入式Linux-守护进程

1.守护进程 1.1 何为守护进程 守护进程&#xff08;Daemon&#xff09;也称为精灵进程&#xff0c;是运行在后台的一种特殊进程&#xff0c;它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生&#xff0c;主要表现为以下两个特点&#xff1a; 长期运行。 守…

Java中的多线程安全问题

目录 一、什么是线程安全&#xff1f; 二、线程不安全的原因 2.1 从底层剖析count的操作 2.2 线程不安全的原因总结 2.3 JVM内存模型&#xff08;JMM&#xff09; 三、synchronized 关键字-监视器锁monitor lock 3.1 如何加锁&#xff08;Synchronized用法和特性&#xff…

【sklearn】模型融合_堆叠法

Stacking参数含义1. 工具库 & 数据2. 定义交叉验证函数2.1 对融合模型2.2 对单个评估器3. 定义个体学习器和元学习器3.1 个体学习器3.2 元学习器4. 评估调整模型5. 元学习器的特征矩阵5.1 特征矩阵两个问题 & Stacking5.2 StackingClassfier\Regressor参数cv - 解决样本…

C语言 动态通讯录实现(附源码)

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; 前言&#xff1a; 上期博客写了静态通讯录并且答应给大家写一个动态版&#xff0c;这不&#xff0c;它来了&#xff1a; 1.动态版与静态版的区别 静态版的内存空间开辟大小是固定的&#xff0c;放了预期的最…

Leetcode 剑指 Offer II 010. 和为 k 的子数组

题目难度: 中等 原题链接 今天继续更新 Leetcode 的剑指 Offer&#xff08;专项突击版&#xff09;系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 给定一个整数数组和一个整数 k &#xff0c;请找到该数组中和为…

MTBF是什么意思?交换机做MTBF有什么要求?MTTF、MTBF和MTTR的区别是什么?

MTBF&#xff0c;即平均故障间隔时间&#xff0c;英文全称是“Mean Time Between Failure”。是衡量一个交换机的可靠性指标。单位为“小时”。它反映了交换机的时间质量&#xff0c;是体现交换机在规定时间内保持功能的一种能力。具体来说&#xff0c;是指相邻两次故障之间的平…

【考研】2020-Part A 作文(英一)

可搭配以下链接一起学习&#xff1a; 【考研】2018-Part B 作文&#xff08;英一&#xff09;_住在阳光的心里的博客-CSDN博客 目录 一、2020 Part A &#xff08;一&#xff09;题目及解析 &#xff08;二&#xff09;优秀范文 &#xff08;三&#xff09;参考译文 &a…

Ansible playbook 讲解与实战操作

文章目录一、概述二、playbook 核心元素三、playbook 语法&#xff08;yaml&#xff09;1&#xff09;YAML 介绍1、YAML 格式如下2、playbooks yaml配置文件解释3、示例2&#xff09;variables 变量1、facts:可以直接调用2、用户自定义变量3、通过roles传递变量4、 Host Invent…

LINUX---文件

目录第一部分&#xff1a;文件编程一.打开/创建文件二.文件的写入操作三.文件的读取四.文件的光标应用&#xff1a;计算文件的大小第二部分&#xff1a;文件操作原理&#xff1a;一.文件描述符静态文件和动态文件第三部分&#xff1a;文件编程小应用1.实现CP命令2.修改文件3.写…

安卓玩机搞机技巧综合资源-----修改rom 制作rom 解包rom的一些问题解析【二十一】

接上篇 安卓玩机搞机技巧综合资源------如何提取手机分区 小米机型代码分享等等 【一】 安卓玩机搞机技巧综合资源------开机英文提示解决dm-verity corruption your device is corrupt. 设备内部报错 AB分区等等【二】 安卓玩机搞机技巧综合资源------EROFS分区格式 小米红…

【Vue笔记】Vue组件的创建、使用以及父子组件数据通信常见的几种方式

这篇文章&#xff0c;主要介绍Vue组件的创建、使用以及父子组件数据通信常见的几种方式。 目录 一、Vue组件的使用 1.1、局部组件 1.2、全局组件 1.3、动态组件&#xff08;组件动态切换&#xff09; 1.4、缓存组件 &#xff08;1&#xff09;如何缓存组件 &#xff08;…

微服务技术--Nacos与Eureka

eureka注册中心 远程调用的问题 消费者该如何获取服务提供者具体信息&#xff1f; 服务提供者启动时向eureka注册自己的信息eureka保存这些信息消费者根据服务名称向eureka拉取提供者信息 如果有多个服务提供者&#xff0c;消费者该如何选择&#xff1f; 服务消费者利用负载均…

区块链技术1---密码学基础

摘要&#xff1a;BTC属于加密货币&#xff0c;其中必然涉及到密码学的知识&#xff0c;而比特币比较开放&#xff0c;交易记录&#xff0c;交易金额甚至是底层源代码都是对外开放&#xff0c;那么加密使用在何处&#xff1f;这里就来谈一谈1&#xff1a;哈希哈希函数是密码学的…

client-go实战之六:时隔两年,刷新版本继续实战

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 系列文章链接 client-go实战之一&#xff1a;准备工作client-go实战之二:RESTClientclient-go实战之三&#xff1a;Clientsetclient-go实战之四&#xff1a;…

JavaWeb开发(三)3.3——Spring Bean详解

一、Bean的概念 由 Spring IoC 容器负责创建、管理所有的Java对象&#xff0c;这些管理的对象称为 Bean&#xff0c;Bean 根据 Spring 配置文件中的信息创建。 二、基于XML方式管理bean对象 eg&#xff1a; <?xml version"1.0" encoding"UTF-8"?&…

【B-树、B+树、B* 树】多叉平衡搜索树,解决“IO次数”与“树高”问题~

目录 一、为什么会出现B-树&#xff1f; 面试题&#xff1a; 二、什么是B-树&#xff1f; 2.1、B,B-树,B*树 导航 三、B-树的模拟实现 3.1、插入结点分析 3.1.1、根节点的分裂 3.1.2、继续插入数据&#xff0c;分裂子节点 3.2.3、再次插入数据&#xff0c;导致根节点继…

tomcat和apache有什么区别?如何将内网发布到互联网访问?

tomcat、 apache是比较常用的搭建服务器的中间件&#xff0c;它们之间还是有一些区别差异的&#xff0c;我们通常会根据本地应用场景来选择合适的中间件来搭建服务器。在内网本地部署搭建服务器后&#xff0c;还可以通过快解析端口映射方法&#xff0c;将内网应用地址发布到互联…

Android原生检测Selinux的三种方法

本文介绍 3 种检测 Android 设备 SELinux 状态的方法, Java 层检测Selinux已经没有太多意义,因为不是很靠谱,随便一个hook代码就能绕过,所以我要告诉你如何在 C 层完成检测。这几种方法在效率和抵抗mock SELinux State 的技术方面都不相同,因此在使用之前你需要知道每种方…

Windows server——部署DNS服务

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 本章重点 一.DNS概述 1.DNS的诞生 二.DNS的功能 使用域名访问具有以下优点…