智能指针

C++程序设计中,使用堆内存是很繁琐的操作——堆内存的申请和释放都需要程序员自己去管理。虽然说程序员自己管理内存可以提高程序的效率,但是整体来说程序员手动管理内存是比较麻烦的,而且容易出现内存泄漏,异常安全(如果在malloc和free之间如果存在抛异常,那么还是有内存泄漏)等问题。而在C++11中引入了智能指针的概念来管理堆内存。

智能指针的实现采用了一种RAII(利用对象生命周期来控制程序资源)的技术对普通的指针进行封装,使得智能指针实际是一个对象,其行为表现的却像一个指针。也就是说,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。那么我们就不需要显式地去释放资源,且对象所需要的资源在生命周期内始终有效。

auto_ptr

简单模拟实现:

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

	AutoPtr(AutoPtr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}

	AutoPtr<T>& operator=(AutoPtr<T>& sp)
	{
		if(this != sp)
		{
			if(_ptr)
				delete _ptr;
		 	//转移资源到当前对象
			_ptr = sp._ptr;
			sp._ptr = nullptr;
		}
	}

	~AutoPtr()
	{
		if(_ptr)
			delete _ptr;
	}

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

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

	T& Get()
	{
		return _ptr;	
	}

};

由以上代码可以看到,auto_ptr的实现是利用了管理权转移的思想——一旦发生拷贝,就将ap/sp中资源转移到当前对象中,然后令ap/sp与其所管理资源断开联系,这样就解决了一块空间被多个对象使用而造成程序奔溃问题。

但是, 通过实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空,通过ap对象访问资源时就会出现问题,于是,引出了一种资源管理权转移的方式

template<class T>
class Autoptr
{
public:
	Autoptr(T* ptr=nullptr)
		:_ptr(ptr)
		,_owner(false)
	{
		if(_ptr)
			_owner = true;
	}

	Autoptr(const Autoptr<T>& ap)
		:_ptr(ap._ptr)
		,_owner(ap._owner)
	{
		ap._owner = false;
	}

	Autoptr<T>& operator=(const Autoptr<T>& sp)
	{
		if(this !=&sp)
		{
			Test();
			_ptr = sp._ptr;
			_owner = sp._owner;
			sp._owner = false;
		}

		return this;
	}

	~Autoptr()
	{
		Test();
	}

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

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

	T& Get()
	{
		return _ptr;	
	}

private:
	void Test()
	{
		if(_ptr && _owner)
		{
			delete _ptr;
			_owner = false;
		}
	}

private:
	T* _ptr;
	mutable bool _owner;
};

unique_ptr

因为在拷贝时经常会出现一些问题,所以引出了一种简单粗暴的方式——禁止拷贝,就问你怕不怕!!!

也就是说,将类中的拷贝构造函数和运算符重载函数设为私有的,使得资源独占,只能被一个对象使用

模拟实现:
template<calss T>
class Uniqueptr
{
public:
	Uniqueptr(T* ptr = nullptr)
		:_ptr(ptr)
	{}

	~Uniqueptr()
	{
		if(_ptr)
			delete _ptr;
	}

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

	T* operator->()
	{
		return _ptr;
	}
private:
	//C98格式
	Uniqueptr(const Uniqueptr<T>& );
	Uniqueptr<T> operator=(const Uniqueptr& );


	//C11格式
	Uniqueptr(const Uniqueptr<T>& ) = delete;
	Uniqueptr<T> operator=(const Uniqueptr& ) = delete;
private:
	T* _ptr;
};

shared_str:

在C++11中,引入了shared_str这一更靠谱且支持拷贝的智能指针。

shared_str的原理是:

  1. 通过引用计数的方式来实现多个shared_str对象之间共享资源。也就是说:shared_str在其内部,为每个资源都维护了一份计数,用来记录该资源被几个对象共享。

  2. 在对象被销毁时,就说明该对象不使用这一资源了,对象的引用计数减一。

  3. 当引用计数减为0时,说明自己是最后一个使用该资源的对象必须释放该资源。

  4. 如果不是0,说明该资源还被其他的对象使用着,就不能释放该资源,否则其他对象就会成为野指针
    //采用引用计数的方式
    //为了线程安全,需要上锁,但是可能会造成死锁(锁内调用的其他代码如果抛异常则会造成死锁),守卫锁,进入时创建对象,函数退出时自动销毁
    // 模拟实现一份简答的SharedPtr,了解原理

    #include <thread>
    #include <mutex>
    template <class T>
    class SharedPtr
    {
    public:
     SharedPtr(T* ptr = nullptr)
     	: _ptr(ptr)
    	 , _pRefCount(new int(1))
     	, _pMutex(new mutex)
     {
     	// 如果是一个空指针对象,则引用计数给0
     	if (_ptr == nullptr)
     	*_pRefCount = 0;
     }
     ~SharedPtr() 
     {
     	Release();
     }
     
     SharedPtr(const SharedPtr<T>& sp)
     	: _ptr(sp._ptr)
     	, _pRefCount(sp._pRefCount)
     	, _pMutex(sp._pMutex)
     {
     	// 如果是一个空指针对象,则不加引用计数,否则才加引用计数
     	if (_ptr)
     	AddRefCount();
     }
     
     // sp1 = sp2
     SharedPtr<T>& operator=(const SharedPtr<T>& sp)
     {
     	//if (this != &sp)
     	if (_ptr != sp._ptr)
     	{
    		 // 释放管理的旧资源
    		 Release();
    	 	// 共享管理新对象的资源,并增加引用计数
    	 	_ptr = sp._ptr;
    	 	_pRefCount = sp._pRefCount;
    		 _pMutex = sp._pMutex;
     		if (_ptr)
     			AddRefCount();
     	}
     	return *this;
     }
     
     T& operator*() 
     {
     	return *_ptr;
     }
     
     T* operator->() 
     {
     	return _ptr;
     }
     
     int UseCount() 
     {
     	return *_pRefCount;
     }
     
     T* Get() 
     {
     	return _ptr; 
     }
     
     int AddRefCount()
     {
     	// 加锁或者使用加1的原子操作
     	_pMutex->lock();
     	++(*_pRefCount);
     	_pMutex->unlock();
     	return *_pRefCount;
     }
     
     int SubRefCount()
     {
     	// 加锁或者使用减1的原子操作
     	_pMutex->lock();
     	--(*_pRefCount);
     	_pMutex->unlock();
     	return *_pRefCount;
     }
     
    private:
     void Release()
     {
     	// 引用计数减1,如果减到0,则释放资源
     	if (_ptr && SubRefCount() == 0)
     	{
     		delete _ptr;
     	delete _pRefCount;
     	}
     }
     
    private:
     	int* _pRefCount; // 引用计数
     	T* _ptr; // 指向管理资源的指针 
     	mutex* _pMutex; // 互斥锁
    };
    
    int main()
    {
    	SharedPtr<int> sp1(new int(10));
    	SharedPtr<int> sp2(sp1);
    	*sp2 = 20;
    	 
    	cout << sp1.UseCount() << endl;
     	cout << sp2.UseCount() << endl;
    	 
     	SharedPtr<int> sp3(new int(10));
     	sp2 = sp3;
     	cout << sp1.UseCount() << endl;
     	cout << sp2.UseCount() << endl;
    	cout << sp3.UseCount() << endl;
    	 
    	sp1 = sp3;
    	cout << sp1.UseCount() << endl;
    	cout << sp2.UseCount() << endl;
    	cout << sp3.UseCount() << endl;
    	return 0;
    }
    

std::shared_str的线程安全问题:

通过以上的代码细心的同学就会发现了:在对引用计数进行加1,减1操作时我们对其进行了上锁操作。

那么为什么要对其进行上锁操作呢?细想一下其实也很简单:

1.智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2。这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作必须是线程安全的。

2.智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题

std::shared_str的循环引用问题:

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

在上边代码上可以看出一个问题——循环引用,分析如下:

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的next指向node2,node2的prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是next还指向下一个节点。但是prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是next属于node的成员,node1释放了,next才会析构,而node1由prev管理,prev属于node2
    成员,所以这就叫循环引用,谁也不会释放。
    // 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
    // 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加
    node1和node2的引用计数。
    struct ListNode
    {
     int _data;
     weak_ptr<ListNode> _prev;
     weak_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;
    }



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值