【C++】智能指针

在这里插入图片描述

🚀write in front🚀
📜所属专栏: C++学习
🛰️博客主页:睿睿的博客主页
🛰️代码仓库:🎉VS2022_C语言仓库
🎡您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我你们将会看到更多的优质内容!!

在这里插入图片描述

前言

  在异常的学习里面我们知道,在抛出异常并接收异常之后,程序就会到处跳转,然后接收,就像下面的代码,抛出异常之后堆上面的资源就没有机会释放了:

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;
}

  这样就会导致内存泄漏的问题出现。

一.智能指针的使用及原理

RAII:

   RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
  在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  1. 不需要显式地释放资源
  2. 采用这种方式,对象所需的资源在其生命期内始终保持有效

智能指针的原理

  其实简单的说,智能指针就是将指针封装到一个类里面去,由于类在程序结束时肯定会调用析构函数,由此来解决前面提到的异常问题。智能指针的原理如下:

  1. 满足RAII
  2. 像指针一样的操作(其实在之前的iterator模拟实现,就像指针一样的操作)

二.智能指针的分类:

  下面的代码都会以这个A对象为例子来打印析构函数来观察是否析构:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}

	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	int _a;
};

auto_ptr

auto_ptr是C++98里面的,大概实现如下:

template<class T>
	class auto_ptr
	{
	private:
		T* _ptr;
	public:
		auto_ptr(T* ptr):_ptr(ptr)
		{}

		~auto_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

		T& operator*()
		{
			return *_ptr;
		}

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

		// ap3(ap1)
		// 管理权转移
		auto_ptr(auto_ptr<T>& p):_ptr(p._ptr)
		{
			p._ptr = nullptr;
		}
	};
	
void test_auto_ptr()
{
	//zxr::auto_ptr<A> ap1(new A(1));
	//ap1->_a;
	//->这里本来应该是->()->,因为第一个->是得到地址,第二个才是访问对象,但是这里简化了,直接一个->搞定了。

	// C++98 一般实践中,很多公司明确规定不要用这个
	zxr::auto_ptr<A> ap1(new A(1));
	zxr::auto_ptr<A> ap2(new A(2));

	// 管理权转移,拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象
	// 隐患:导致被拷贝对象悬空,访问就会出问题
	zxr::auto_ptr<A> ap3(ap1);
	//崩溃
	ap1->_a++;
	//由于置为空,访问就崩溃了
}

  然而,auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr,因为我们可以从拷贝构造函数看出在拷贝之后,用来拷贝的对象资源被抢走,自己变为空了,这里就是一个很大的问题,所以在后面的C++11里面,就是要解决这个问题。

unique_ptr

  终于,在C++11对auto_ptr进行了一定程度的改变,如果我们用的指针不想要拷贝,那就可以使用unique_ptr,因为他暴力的实现了防拷贝:

template<class T>
	class unique_ptr
	{
	private:
		T* _ptr;
	public:
		unique_ptr(T* ptr) :_ptr(ptr)
		{}

		~unique_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

		T& operator*()
		{
			return *_ptr;
		}

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

		//防止调用某成员函数的方法:
		//1.放到private里面去
		//2.在后面加一个delete

		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;

	};
	void test_unique_ptr()
{
	// C++11  简单粗暴,不让拷贝
	zxr::unique_ptr<A> up1(new A(1));
	zxr::unique_ptr<A> up2(new A(2));

	/*zxr::unique_ptr<A> up3(up1);
	up1 = up2;*/
	//此时显示:已是删除的函数
}

shared_ptr:

  当然,C++11也提供了靠谱的并且支持拷贝的智能指针shared_ptr。
  shared_ptr的原理是通过引用计数的方式来实现多个shared_ptr对同一块空间的管理:

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

下面我们就来模拟实现一下:

template<class T>
	class shared_ptr
	{
	private:
		T* _ptr;
		int* _count;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };

	public:
		shared_ptr(T* ptr)
			:_ptr(ptr),
			_count(new int(1))
		{}
		
		template<class D>
		shared_ptr(T* ptr, D del) 
			:_ptr(ptr),
			_count(new int(1)),
			_del(del)
		{}

		~shared_ptr()
		{
			if (--(*_count) == 0)
			{
				cout << "delete:" << _ptr << endl;
				_del(_ptr);
				delete _count;
			}	
		}

		T& operator*()
		{
			return *_ptr;
		}

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

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


		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			//自己给自己赋值,或者指向一个空间的两个值赋值:
			if (sp._ptr == _ptr)
			{
				return *this;
			}

			//将计数-1,如果等于0,就手动释放这个空间
			if (--(*_count) == 0)
			{
				delete _ptr;
				delete _count;
			}

			//将sp1赋值给this
			_ptr = sp._ptr;
			_count = sp._count;
			++(*_count);

			return *this;
		}

		int use_count()const
		{
			return *_count;
		}

		T* get()const
		{
			return _ptr;
		}
	};
void test_shared_ptr()
{
		// C++11
	zxr::shared_ptr<A> sp1(new A(1));
	zxr::shared_ptr<A> sp2(new A(2));

	zxr::shared_ptr<A> sp3(sp1);
	sp1->_a++;
	sp3->_a++;

	cout << sp1->_a << endl;

	zxr::shared_ptr<A> sp4(sp2);
	zxr::shared_ptr<A> sp5(sp4);

	sp1 = sp5;
	sp3 = sp5;

	zxr::shared_ptr<A> sp6(new A(6));
	sp6 = sp6;
	sp4 = sp5;

	// cout << sp6->_a << endl;

}

循环引用:

  当然,使用shared_ptr在有的时候也会出问题,我们以链表为例:
  如果我们想通过智能指针来实现链表,那么他的基本实现应该是这样的:

struct Node
{
	A _val;
	zxr::shared_ptr<Node> _next;
	zxr::shared_ptr<Node> _prev;
};

void test_weak_ptr()
{


	zxr::shared_ptr<Node> sp1(new Node);
	zxr::shared_ptr<Node> sp2(new Node);

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	sp1->_next = sp2;
	sp2->_prev = sp1;

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
}

&emps; 但是此时我们在打印时会发现,没有调用析构函数!
在这里插入图片描述
  这是为什么呢?
在这里插入图片描述

  1. node1node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete
  2. node1的_next指向node2node2的_prev指向node1,引用计数变成2。
  3. node1node2析构,引用计数减到1,此时由于对Node没有进行delete操作,就无法对_prev和_next进行释放。_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node2的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,node2释放了,_prev才会析构,而node2由_next管理,但是_next属于node2的成员…所以这就叫循环引用,谁也不会释放.

解决方案:将结点里面的shared_ptr改成weak_ptr

struct Node
{
	
	A _val;
	weak_ptr<Node> _next;
	weak_ptr<Node> _prev;
};

下面我们就来介绍一下weak_ptr:

weak_ptr

  weak_ptr不是RAII智能指针,而是专门用来解决shared_ptr循环引用问题

  weak_ptr不增加引用计数可以访问资源不参与资源释放的管理。由于其是专门解决引用计数问题,就要可以通过share_ptr拷贝构造:

// weak_ptr不是RAII智能指针,专门用来解决shared_ptr循环引用问题
	// weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理
	template<class T>
	class weak_ptr
	{
	private:
		T* _ptr=nullptr;
	public:
		weak_ptr()
		{}

		~weak_ptr()
		{
			cout << "hh" << endl;
		}
		

		T& operator*()
		{
			return *_ptr;
		}

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

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

	};

  由于他不能像其他智能指针一样通过指针构造,他就只能通过shared_ptr来赋值,所以他只能用于解决循环引用的问题。

四种智能指针的特点:

在这里插入图片描述

三.定制删除器:

  在上面的实现里面不知道大家有没有发现,对于指针的删除全都是delete _ptr,但是如果我们要删除一个数组怎么办呢?

  其实在shared_ptr里面,我们在构造的时候还应该传一个函数对象,通过函数对象来对不同种类的指针进行不同的释放。
在这里插入图片描述
  由于得到的这个函数对象是在构造函数里面,我们要在析构函数里面使用,我们就要将其保存在类里面,此时就要用到我们之前学到的包装器了(可以包装那三种类型的函数对象)。

template<class T>
	class shared_ptr
	{
	private:
		T* _ptr;
		int* _count;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };

	public:

		shared_ptr(T* ptr=nullptr)
			:_ptr(ptr),
			_count(new int(1))
		{}
		
		//由于不知道传过来的是哪一种函数对象,我们就通过模板来识别。
		template<class D>
		shared_ptr(T* ptr, D del) 
			:_ptr(ptr),
			_count(new int(1)),
			_del(del)
		{}

		~shared_ptr()
		{
			if (--(*_count) == 0)
			{
				cout << "delete:" << _ptr << endl;
				_del(_ptr);
				delete _count;
			}	
		}

		T& operator*()
		{
			return *_ptr;
		}

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

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


		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			//自己给自己赋值,或者指向一个空间的两个值赋值:
			if (sp._ptr == _ptr)
			{
				return *this;
			}

			//将计数-1,如果等于0,就手动释放这个空间
			if (--(*_count) == 0)
			{
				delete _ptr;
				delete _count;
			}

			//将sp1赋值给this
			_ptr = sp._ptr;
			_count = sp._count;
			++(*_count);

			return *this;
		}

		int use_count()const
		{
			return *_count;
		}

		T* get()const
		{
			return _ptr;
		}
		
	};
//定制删除器
void test_delete()
{
	zxr::shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[]ptr; });
	zxr::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
	zxr::shared_ptr<FILE> sp3(fopen("test.cpp", "r"), [](FILE* fin) {cout << "fclose:"; fclose(fin); });
}

当然在这里也可以灵活使用,就比如还可以用于文件操作的打开与关闭

四.C++11和boost中智能指针的关系:

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost(第三方库)给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的

总结

 7emsp其实智能指针就是将指针的释放放入了一个类里面,通过类在程序结束时肯定会调用析构函数的特性来解决程序异常结束而不能释放空间的问题。

  更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

专栏订阅:
每日一题
C语言学习
算法
智力题
初阶数据结构
Linux学习
C++学习
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

坤小满

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

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

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

打赏作者

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

抵扣说明:

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

余额充值