C++智能指针

●🧑个人主页:你帅你先说.
●📃欢迎点赞👍关注💡收藏💖
●📖既选择了远方,便只顾风雨兼程。
●🤟欢迎大家有问题随时私信我!
●🧐版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。

1.为什么需要智能指针?

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
int* p1 = new int;
int* p2 = nullptr, * p3 = nullptr;
int div()
{
	int a = 3, b = 0;

	if (b == 0)
		throw invalid_argument("除0错误");
	//若此时抛出了异常, 那下面这段代码就没法执行了,就会导致内存泄漏
	delete p1;
	delete p2;
	delete p3;
	return a / b;
}
void func()
{
	
	try
	{
		p2 = new int;
		p3 = new int;

		cout << div() << endl;
	}
	catch (...)
	{

	}

}
int main()
{
	func();
}

智能指针用来解决内存泄漏异常安全问题

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

2.1RAII

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

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

2.2智能指针原理

使用RAII思想设计智能指针

template<class T>
class SmartPtr
{
public:
	//构造函数
	SmartPtr(T* ptr):_ptr(ptr)
	{}
	//析构函数
	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}
	//重载*运算符
	T& operator*()
	{
		return *_ptr;
	}
	//重载->运算符
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

智能指针的原理:
1.RAII特性
2.重载operator*和opertaor->,具有像指针一样的行为。

2.3常见的智能指针

2.3.1auto_ptr

实现原理:管理权转移
我们直接来看模拟实现

template<class T>
class auto_ptr
{
public:
	//构造函数
	auto_ptr(T* ptr):_ptr(ptr)
	{}
	//拷贝构造函数
	auto_ptr(auto_ptr<T>& sp)
		:_ptr(sp._ptr)
	{
		// 管理权转移
		sp._ptr = nullptr;
	}
	//析构函数
	~auto_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}

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

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

但实际上auto_ptr在实际中很少使用,因为它的设计有缺陷。
缺陷:原指针被置空了,可能会出现对空指针进行解引用。

2.3.2unique_ptr

实现原理:防拷贝

template<class T>
class unique_ptr
{
public:
	//构造函数
	unique_ptr(T* ptr):_ptr(ptr)
	{}
	//析构函数
	~unique_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}

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

	T* operator->()
	{
		return _ptr;
	}
	//防止拷贝
	unique_ptr(const unique_ptr<T>& sp) = delete;
private:
	T* _ptr;
};

但即便这样设计还是有问题,万一在某种场景下我需要拷贝。所以就有了接下来的shared_ptr。
缺陷:针对于需要拷贝的场景不能很好的处理。

2.3.3shared_ptr

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

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

	shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pRefCount(sp._pRefCount), _pmtx(sp._pmtx)
	{
		AddRef();
	}

	void Release()
	{
		//加锁保证线程安全
		_pmtx->lock();

		bool flag = false;
		if (--(*_pRefCount) == 0 && _ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _pRefCount;

			flag = true;
		}

		_pmtx->unlock();

		if (flag == true)
		{
			delete _pmtx;
		}
	}

	void AddRef()
	{
		_pmtx->lock();

		++(*_pRefCount);

		_pmtx->unlock();
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//if (this != &sp)
		//这里不能写成this != &sp,因为可能会出现指向同一个对象的指针相互赋值,这样就没有意义了,下面这样写可以提高效率。
		if (_ptr != sp._ptr)
		{
			Release();

			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			_pmtx = sp._pmtx;
			AddRef();
		}

		return *this;
	}

	int use_count()
	{
		return *_pRefCount;
	}

	~shared_ptr()
	{
		Release();
	}

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

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

	T* get() const
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pRefCount;
	mutex* _pmtx;
};

几点注意:

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

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

你会发现weak_ptr的设计相较于shared_ptr其实就是取消了引用计数。
循环引用场景:

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

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);
	// 循环引用
	n1->_next = n2;
	n2->_prev = n1;

	return 0;
}

这段代码你可能没看出来什么问题,一张图带你了解。
在这里插入图片描述

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

解决方案:把结构体内的shared_ptr换成weak_ptr就可以解决了。

struct ListNode
{
	int _val;
	std::weak_ptr<ListNode> _next;
	std::weak _ptr<ListNode> _prev;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);
	// 循环引用
	n1->_next = n2;
	n2->_prev = n1;

	return 0;
}

喜欢这篇文章的可以给个一键三连点赞👍关注💡收藏💖

  • 12
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

你帅你先说.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值