智能指针

C++中没有垃圾回收器,所以对于我们自己申请的内存必须自己来释放,而且要正确释放。但是当程序代码较为庞杂的时候,对于指针的管理开始变得困难起来,申请多个指针,不免会有疏漏而忘记释放,程序异常的抛出导致的跳转而遗漏等等,那么,如何高效的管理指针就成为当前一个棘手的问题。

为解决这个问题,我们的前辈得出了智能指针的这个方法,智能指针到底怎样实现智能呢?下面我们一起来看看下面这段代码

#include<malloc.h>
class SmartPtr
{
public:
	SmartPtr(int *ptr=nullptr):_ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	int *_ptr;
};
int main()
{
	int *temp = (int*)malloc(sizeof(int));
	SmartPtr sp(temp);
	return 0;
}

智能指针其本质就是利用类对象的生命周期来控制程序资源,当程序返回时,类对象自动调用类的析构函数,来达到自动释放空间,这样的行为看起来是smart的,因此称它为智能指针。

但SmartPtr这样还没有完全具有指针的行为(解引用,指向)。对于智能指针有了一个基本的认知,库中的智能指针的根本思想和上面的SmartPtr一致,接下来,我们来模拟实现库中的AutoPtr

#include<malloc.h>
#include<iostream>
using namespace std;
class Person
{
public:
	Person(int age=5) :_age(age) {}
	int _age;
};
class AutoPtr
{
public:
	AutoPtr(Person *ptr=nullptr):_ptr(ptr)
	{}
	AutoPtr(AutoPtr& sp)
		:_ptr(sp._ptr)
	{
	   sp._ptr = NULL;
	}
	AutoPtr operator=(AutoPtr& sp)
	{
		if (this != &sp)//判断是否是自己给自己赋值
		{
			if (_ptr)//释放当前对象资源
				delete _ptr;

			_ptr = sp._ptr;//转移资源到被赋值对象
			sp._ptr = NULL;
		}	
	}
	~AutoPtr()
	{
		if (_ptr)
			delete _ptr;
	}

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

int main()
{
	AutoPtr ptr(new Person);
	AutoPtr pcr(ptr);
	ptr->_age = 10;
	return 0;
}

当管理指针的对象被拷贝或者赋值时,会导致一份资源被多个对象引用时,在释放的时候会多次调用析构函数,导致一份资源被多次释放,这是不合理的。AutoPtr在处理拷贝和赋值问题上使用了资源转移的思想来解决这个问题,但以这样的方式处理问题又带来了新的问题,当对象的资源被转移,对象的指针被置空,但当程序未返回时,这个空指针一直存在,这可能会导致误用空指针,所以AutoPtr是不安全的,为了解决这个问题,我们又有了UniquePtr。

UniquePtr的方式很直接有效,那就是禁止掉拷贝和赋值这两种操作,下面看代码:

#include<malloc.h>
#include<iostream>
using namespace std;
class Person
{
public:
	Person(int age = 5) :_age(age) {}
	int _age;
};
class UniquePtr
{
public:
	UniquePtr(Person *ptr = nullptr) :_ptr(ptr)
	{}
	~UniquePtr()
	{
		if (_ptr)
			delete _ptr;
	}

	Person& operator*()
	{
		return *_ptr;
	}
	Person* operator->()
	{
		return _ptr;
	}
private:
	//C++98
	UniquePtr(UniquePtr const &up);
	UniquePtr& operator = (UniquePtr const &up);
		//C=+11
	UniquePtr(UniquePtr const &up) = delete;
	UniquePtr& operator = (UniquePtr const &up) = delete;
private:
	Person *_ptr;
};

这种方式是不太合理的,一个类对象不能够进行拷贝和赋值,极大的降低了灵活性,当我们需要这种操作时,被禁止掉是不科学的,所以在库中也提供了可以进行拷贝和赋值操作的SharePtr,为了简单的了解原理,下面我们模拟实现一下:

#include<iostream>
#include<thread>
#include<mutex>

using namespace std;

template<class T>

class SharedPtr
{
public:
	SharedPtr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pCount(new int(1))
		, _pMutex(new mutex)
	{
		//如果_ptr是一个空指针则将引用计数置0
		if (_ptr==nullptr)
			*_pCount = 0;
	}
	SharedPtr(const SharedPtr<T>& sp)
		:_ptr(sp._ptr)
		,_pCount(sp._pCount)
		,_pMutex(sp._pMutex)
	{
		//如果_ptr不是一个空指针,则对引用计数+1
		if (_ptr)
			AddCount();
	}
	SharedPtr<T>& operator=(const SharedPtr<T>& sp)
	{
		//除去本身赋值
		if (this != &sp)
		{
			Release();

			_ptr = sp._ptr;
			_pCount = sp._pCount;
			_pMutex = sp._pMutex;

			AddCount();
		}
		return *this;
	}
	int AddCount()
	{
		//加锁保证操作原子性
		_pMutex->lock();
		++(*_pCount);
		_pMutex->unlock();
		
		return *_pCount;
	}
	int SubCount()
	{
		_pMutex->lock();
		--(*_pCount);
		_pMutex->unlock();
		
		return *_pCount;
	}
	int GetCount() { return *_pCount; }
	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }
	~SharedPtr() { Release(); }

private:
	void Release()
	{
		if (_ptr&&SubCount() == 0)
		{
			delete _ptr;
			delete _pCount;
		}
	}
private:
	T* _ptr;
	int *_pCount;
	mutex* _pMutex;
};


int main()
{
	SharedPtr<int>temp;
	cout << temp.GetCount() << endl;
	SharedPtr<int>sp1(new int(6));
	cout << sp1.GetCount() << endl;
	SharedPtr<int>sp2(sp1);
	cout << sp1.GetCount() << endl;
	cout << sp2.GetCount() << endl;
	SharedPtr<int>sp3(new int(10));
	sp1 = sp3;
	cout << sp1.GetCount() << endl;
	cout << sp2.GetCount() << endl;
	cout << sp3.GetCount() << endl;
	sp2 = sp3;
	cout << sp1.GetCount() << endl;
	cout << sp2.GetCount() << endl;
	cout << sp3.GetCount() << endl;
	return 0;
}

PS:以上的三种智能指针包含于头文件<memory>

SharePtr看起来似乎可以达到我们几百年使用要求,但它还存在一个致命的问题,循环引用导致的资源泄露:

#include<iostream>
#include<memory>
using namespace std;

struct ListNode {
	int _data;    
	shared_ptr<ListNode> _prev; 
	shared_ptr<ListNode> _next;

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

int main() 
{
	shared_ptr<ListNode> node1(new ListNode);  
	shared_ptr<ListNode> node2(new ListNode);   
	cout << node1.use_count() << endl;  
	cout << node2.use_count() << endl;

	node1->_next = node2; 
	node2->_prev = node1;


	cout << node1.use_count() << endl; 
	cout << node2.use_count() << endl;

	return 0;
}

node1和node2析构,引用计数都减为1;

node1的next引用node2,next析构了,node2才能析构;

node2的prev引用node1,prev析构了,node1才能析构;

现在你会发现node1和node2形成了一个相互制约的局面,node1管理的next1,但node1想要析构必须听prev的,而prev由node2管理,而node2能否析构又由node1管理的next说了算,它们之间形成了一个闭环,而且这两个对象也无法再被访问到,如果没有针对这种情况的处理办法,它们将一致僵持下去,谁也不能够释放资源,导致资源泄露,这种情况和产生死锁中的环路等待非常类似,但在C++中,这种情况并不叫做死锁,这种现象被称为循环引用。

强引用和弱引用?

强引用是它所引用的对象存在时,这个引用也存在,share_ptr就是强引用;相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它自身存在时的一个引用

为了解决循环引用的问题,我们引入了weak_ptr,原理不再深究,掌握使用方法即可:

weak_ptr必须是由share_ptr或者另一个weak_ptr转换而来

对于上面的循环引用,只要将一方的强引用修改为弱引用即可打破循环引用:

struct ListNode {
	int _data;    
	weak_ptr<ListNode> _prev; 
	shared_ptr<ListNode> _next;

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

关于weak_ptr:

通过weak_ptr可以有效的解除循环引用,但这种方式必须在程序员能预见可能出现循环引用的情况下才能使用,可见使用智能指针并不能完全杜绝内存泄漏问题,因此,对于内存问题我们必须谨慎处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值