拿捏C++智能指针


前言

在本篇文章中我们将会讲解内存泄漏以及解决一下c++异常处理的一些复杂场景,从而引出智能指针。

一、内存泄漏

内存泄漏:因为疏忽或者错误导致不再使用的内存没有被释放,并不是指物理内存上的丢失,而是程序分配某段内存后,因为设计错误,失去了对该段内存的控制,从而导致内存泄漏

内存泄露的危害

内存泄露会导致相应的部分不能够能使用,在长期运行的程序中,比如操作系统,服务器等,就会导致慢慢变卡的情况,严重的话甚至导致进程退出。就比如玩着王者突然卡死的情况,这是很难受的!!!

我们从代码的角度看一下

void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
	// 2.异常安全问题
	int* p3 = new int[10];
	Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
	delete[] p3;
}

内存泄漏的分类

🌟 🌟 堆内存泄露
我们在使用malloc/realloc/calloc/new等申请堆上的空间时,如果没有对应的free/delete进行释放,对应的内存没有被释放,这部分空间将无法被使用,导致内存泄漏

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

如何避免内存泄露

🌟 🌟 事先预防
我们在使用相应的函数时,一定要注意匹配使用,避免内存泄漏。良好的编码规范。如果异常释放不了,配合智能指针释放资源。
有的公司内部规范使用内部实现的私有内存管理库,这些库自带内存泄漏检测工具。

🌟 🌟 事后处理
采用一些内存泄露的工具检测。

二、智能指针

1.简单介绍

我们介绍智能指针之前,看一下这种场景如果我们采用异常的处理该如何解决??

int *p1=new int[ 10 ];
int *p2=new int[ 10 ];
int *p3=new int[ 10 ];

如果p1,p2,p3都申请到内存,正常释放,我们后续的代码应该是这样的

delete [ ]p1;
delete [ ]p2;
delete [ ]p3;

new会进行异常检查

那如果p1或者p2或者p3没有申请到内存,按照我们处理异常的方式,按照下面写法

void fun()
{
	int* p1;
	int* p2;
	int* p3;

	int* p1 = new int[10];

	try
	{
		p2 = new int[10];
		try
		{
			p3 = new int[10];
		}
		catch (...)
		{
			delete[]p1;
			delete[]p2;
			throw;
		}
	}
	catch (...)
	{
		delete[]p1;
		throw;
	}
	delete[]p1;
	delete[]p2;
	delete[]p3;

}

总看起来很别扭,我们可以通过一种更好的方式解决这个问题—智能指针

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

对象构造时获取资源,在对应的生命周期内对资源进行控制和管理,对象析构时释放空间。
把一份资源的管理交给了一个对象

有两个好处
🌟 🌟 不用显式释放资源
🌟 🌟 所需的资源在其生命周期始终有效

实际上智能指针就是通过一个类来实现的,通过具有指针的功能。

namespace peng
{
	template<class T>
	class SmartPtr
	{
	public:
		SmartPtr(T* ptr = nullptr)
			:_ptr(ptr)
		{
			cout << "SmartPtr(T* ptr == nullptr)" << endl;
		}

		~SmartPtr()
		{
			cout << "	~SmartPtr()" << endl;
		    delete _ptr;
		}
		//解引用
		T& operator*()
		{
			return *_ptr;
		}
		//->
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

我们运行看一下
在这里插入图片描述

正常析构,释放资源。为什莫呢???
因为如果出现了异常,程序会跳到catch的地方,就出了作用域,出了作用域,生命周期结束,自动调用析构函数,资源就得到释放。
同时具有指针的功能。我们看一下

在这里插入图片描述

peng::SmartPtrp1(new int(10));这是一个单参数的隐式类型转换。
peng::SmartPtrp2=new int(10);正确赋值。

这里的 p4->_a 原型是p4.operator->() ->。本质是两个箭头,但是C++委员会做了特殊处理,只需要一个->就可以。

2.auto_ptr

我们还有求智能指针具有拷贝和赋值的功能,那应该如何实现呢???
我们采用深拷贝呢,还是浅拷贝???
比如list/vector等,利用资源存储管理数据,资源是自己的,拷贝时,每个对象1各自一份资源,各自管理自己的,所以深拷贝。
智能指针,迭代器,资源不是自己的马,只是代为持有,方便访问修改数据,拷贝时期望指向同一块资源

C++98研究出了auto_ptr的方法。

管理权转移

我们通过代码看一下

在这里插入图片描述
代码走到71行,p1获取资源进行初始化,72行p2由p1拷贝而来。

在这里插入图片描述
我们可以发现拷贝之后,本来p1的资源现在被转移到了p2中,同时p1被清空,这也就意味着我们之后不能再对p1的值进行操作,p1悬空。很多公司都不使用这个,因为这个很坑。

我们来模拟实现一下

	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{
			
		}
		~auto_ptr()
		{
	
			delete _ptr;
		}
		//解引用
		T& operator*()
		{
			return *_ptr;
		}
		//->
		T* operator->()
		{
			return _ptr;
		}
		//拷贝构造
		//p2(p1)
		//不能加const
		auto_ptr( auto_ptr<T>&sp)
		{
			_ptr = sp._ptr;
			sp._ptr = nullptr;
		}
		//赋值
		//p2 = p1;
		auto_ptr<T>& operator=(auto_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				//释放p2
				delete _ptr;

				_ptr = sp._ptr;
				sp._ptr = nullptr;
			}
			return *this;
		}
	private:
		T* _ptr;
	};

因为有缺陷,标准委员会建议:什么情况下都不要使用 auto_ptr

2.unique_ptr

unique_ptr是C++11的东西,在c++98到c++11期间,还产生了boost,C++11中unique是在boost中scoped_ptr改装的。
以及后面讲述的shared_ptr,weak_ptr都是在boost的基础上改装的。

我们看一下unique_ptr是如何操作的

在这里插入图片描述

我们可以发现unique_ptr是禁止拷贝的,这仅仅适用于不需要拷贝的场景。

我们来模拟实现一下

	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{

		}
		~unique_ptr()
		{

			delete _ptr;
		}
		//解引用
		T& operator*()
		{
			return *_ptr;
		}
		//->
		T* operator->()
		{
			return _ptr;
		}
		//拷贝构造
		unique_ptr(unique_ptr<T>& sp) = delete;
		//赋值
		//p2 = p1;
		unique_ptr<T>& operator=(unique_ptr<T>& sp) = delete;

	private:
		T* _ptr;
	};

3.shared_ptr

shared_ptr支持拷贝和赋值,但是这是通过引用计数的方式实现的。

通过引用计数的方式实现多个shared_ptr对象之间资源共享

首先简单看一下

在这里插入图片描述

我们想一下这个模拟实现需要怎末弄呢??
多个对象共用一份资源,我们需要用到引用计数的方式,每个资源都维护一份引用计数,当引入一个共用该资源的对象时候,引用计数++,当其中一个对象析构之后,引用计数- -,如果引用计数减到0了,我们就释放这个资源。

这个引用计数应该如何设计呢???
采用静态成员遍历是否可以呢!!!静态成员适用于所有对象,我们的要求是一份资源配一个引用计数。

采用指针充当引用计数

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_count(new int(1))
	{

	}
	//引用计数减少到0,才释放资源
	~shared_ptr()
	{
		if (--(*_count) == 0)
		{
			delete _count;
			delete _ptr;
		}
	}
	//解引用
	T& operator*()
	{
		return *_ptr;
	}
	//->
	T* operator->()
	{
		return _ptr;
	}
	//拷贝构造
	//p2(p1);
	shared_ptr(shared_ptr<T>& sp)
	{
		//拷贝
		_ptr = sp._ptr;
		_count = sp._count;
		//加加引用计数
		++(*_count);
	}
	//赋值
	//p2 = p1;
	shared_ptr<T>& operator=(shared_ptr<T>& sp)
	{
		//判断是否自己给自己赋值,判断_ptr
		if (_ptr != sp._ptr)
		{
			if (--(*_count) == 0)
			{
				delete _count;
			   delete _ptr;
			}
			拷贝
			_ptr = sp._ptr;
			_count = sp._count;
			加加引用计数
			++(*_count);
			//不行
			//(*_count)++;
		}
		return *this;
	}
private:
	T* _ptr=nullptr;
	int* _count;
};

4.weak_ptr

我们使用shared_ptr可以解决拷贝和赋值的问题,但是它有一个小缺陷
看一下下面这个例子

struct ListNode
{
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	shared_ptr<ListNode> n1(new ListNode());
	shared_ptr<ListNode> n2(new ListNode());

	n1->_next = n2;
	n2->_prev = n1;
	
	return 0;
}

我们运行一下,看一下资源释放了没有

在这里插入图片描述

如果我们屏蔽掉 n1->_next = n2;n2->_prev = n1;二者中一个代码资源就可以正确释放

在这里插入图片描述

我们先来看看屏蔽一个为什莫这可以正确释放,画图理解一下

前两行代码,构造时获取资源

在这里插入图片描述
n2->_prev=n1;
在这里插入图片描述

两个资源析构,n2率先析构,n1析构,n2引用计数减为0,n1引用计数减为1。
因为n2的引用计数减为0,ListNode进行释放,_prev也要释放,_prev指向n1,n1要进行析构,n1的引用计数减为0,n1的ListNode顺利释放,同时n2的ListNode也顺利释放。不会造成内存泄漏。

在这里插入图片描述

n1->_next = n2;
n2->_prev = n1;

n1和n2的引用计数都变为2

在这里插入图片描述
n2析构,n1析构,两个的引用计数都减为1
n1要释放资源,n1靠n2的_prev管理着,所以首先n2的_prev要先进行释放,n1才可以释放。
n2的_prev什么时候释放??n2进行析构的时候,才释放。
n2的资源释放又要借助n1,因为n2是被n1的 _next管理着。
那么n1的 _next什么时候释放呢??n1释放资源的时候。

我们最终发现,绕了一圈最终又绕回去了,所以两个资源都得不到释放。
在这里插入图片描述

解决

上面的错误我们称为循环引用,我们这时就借助wear_ptr解决,wear_ptr里面不存储计数,不参与计数管理。
在这里插入图片描述

那他的底层是如何实现的呢???

我们在实现构造函数时要注意,并不用指针直接构造
在这里插入图片描述

weak_ptr不能单独管理资源,必须配合shared_ptr一块使用,解决shared_ptr中存在的 循环引用问题。不具备RALII的特性。
weak_ptr与shread_ptr的实现方式类似,都是通过引用计数的方式实现的,但底层实现不同,
weak_ptr不增加引用计数,不参与资源的申请和释放,不需要写析构函数


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

		}
		//解引用
		T& operator*()
		{
			return *_ptr;
		}
		//->
		T* operator->()
		{
			return _ptr;
		}
		//拷贝构造
		weak_ptr(shared_ptr<T>& sp)
		{
			//私有
			_ptr = sp.get();
		}
		//赋值
		//p2 = p1;
		weak_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}
	private:
		T* _ptr;
	};

C++11中提供的智能指针都只能管理单个对象的资源,没有提供管理一段空间资源的智能指针
weak_ptr的唯一作用就是解决shared_ptr中存在的循环引用问题

总结

以上就是今天要讲的内容,本文仅仅详细介绍了C++智能指针的内容。希望对大家的学习有所帮助,仅供参考 如有错误请大佬指点我会尽快去改正 欢迎大家来评论~~ 😘 😘 😘

  • 29
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lim 鹏哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值