C++ -- 智能指针 auto_ptr,unique_ptr,shared_ptr的简单实现和原理

一,为什么需要智能指针

智能指针是一种预防型的内存泄漏的解决方案。由于C++没有垃圾回收器机制,所以每次new出来的资源都需要手动的delete,如果没有手动释放,就会造成资源泄漏等问题。因此,为了避免这一问题,C++引入了智能指针,可以较好的解决异常安全等带来的内存泄漏问题。

智能指针的原理:RAII(自动释放资源)+具有指针类似的行为operator*() / operator->()+解决浅拷贝的方式。
所有不同类型的智能指针都包括以下这些内容:
1,RAII:资源可以自动释放
2,具有指针类似的行为:operator*()/operator->()
3,都需要考虑解决浅拷贝的问题

二,RAII
RAII–资源获取时就初始化,将内存的管理交付给了对象,在构造函数中申请资源,在析构函数中释放资源。这样就避免了资源泄漏问题。

简单模拟实现:

template<class T>
class smartptr
{
public:
	smartptr(T* ptr = nullptr)
		:_ptr(ptr)
	{
		cout << "smartptr(T* )" << endl;
	}
	~smartptr()
	{
		cout << "~smartptr(T* )" << endl;
		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};
void testsmartptr()
{
	smartptr<int> sp(new int);
}
int main()
{
	testsmartptr();
	return 0;
}

RAII的作用:用户不用考虑什么时候释放资源,把释放资源的事情交给了编译器

但这样的简单实现会导致浅拷贝,导致一份资源多次释放

template<class T>
class smartptr
{
public:
	smartptr(T* ptr = nullptr)
		:_ptr(ptr)
	{
		cout << "smartptr(T* )" << endl;
	}
	~smartptr()
	{
		cout << "~smartptr(T* )" << endl;
		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
void testsmartptr()
{
	int a = 10;
	int *pa = &a;
	int *pb(pa);
	smartptr<int> sp1(new int);
	smartptr<int> sp2(sp1);//由于没有拷贝构造函数,所以编译器会调用构造函数。sp1和sp2会指向同一块内存空间
}//在函数结束时,sp1,sp2都会释放内存,这是就会导致同一内存多次释放。
int main()
{
	testsmartptr();
	return 0;
}

虽然之前我们所遇到的string类也存在浅拷贝问题,它可以通过深拷贝来解决问题,但是我们这个智能指针却不能通过深拷贝的方式来解决。这是因为string所申请的空间是在类中申请的空间,所以在拷贝构造函数中也只需要在类中为新对象申请一块空间。但是我们所提供的智能指针,它的资源是用户提供的,并不是类自己申请。它没有自己申请的权利,只有释放的权利。因此它不能通过深拷贝的方式进行解决。

三,智能指针的类型

auto_ptr(C++98)

namespace heqing
{
	//auto_ptr解决的原理就是资源的转移
	template<class T>
	class auto_ptr
	{
	public:
		//RAII
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~auto_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}
		//指针特性
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//解决浅拷贝(资源的转移)
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
		auto_ptr<T>& operator=(auto_ptr<T> ap)
		{
			if (this != &ap)
			{
				if (_ptr)//如果当前对象管理资源了,先释放原有的资源
				{
					delete _ptr;
				}

				_ptr = ap._ptr;//资源转移
				ap._ptr = nullptr;//ap与资源断开练习
			}
			return *this;
		}
	private:
		T* _ptr;

	};
}
void testautoptr()
{
	int a = 10;
	int* pa = &a;
	int* pb = pa;
	*pa = 100;
	*pb = 200;//两个对象能同时操作同一份资源
	heqing::auto_ptr<int> ap1(new int);
	heqing::auto_ptr<int> ap2(ap1);
	//资源转移带来的缺陷。两个对象不能同时操作同一份资源
	*ap2 = 200;
	*ap1 = 100;
	heqing::auto_ptr<int> ap3;
	ap3 = ap2;
}
int main()
{
	testautoptr();
	return 0;
}

由于上一个实现是通过资源的转移来进行浅拷贝问题,但这样也导致了两个指针不能同时操作一个资源的问题。所以为了解决这个问题,给出以下解决方案。可以个两个指针操作同一份资源的权利,但释放的权利只能有一个指针拥有。

namespace heqing
{
	//auto_ptr解决的原理就是资源的转移
	template<class T>
	class auto_ptr
	{
	public:
		//RAII
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _owner(false)
		{
			if (_ptr)
			{
				_owner = true;
			}
		}
		~auto_ptr()
		{
			if (_ptr&&_owner)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}
		//指针特性
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
			,_owner(ap._owner)
		{
			ap._owner = false;
		}
		auto_ptr<T>& operator=(auto_ptr<T> ap)
		{
			if (this != &ap)
			{
				if (_ptr&&_owner)//如果当前对象管理资源了,先释放原有的资源
				{
					delete _ptr;
				}

				_ptr = ap._ptr;//资源转移
				_owner = ap._owner;
				ap._owner = false;//ap将释放权利转移给了this
			}
			return *this;
		}
	private:
		T* _ptr;
		bool _owner;

	};
}
void testautoptr()
{
	heqing::auto_ptr<int> ap1(new int);
	heqing::auto_ptr<int> ap2(ap1);
	
	heqing::auto_ptr<int> ap3;
	ap3 = ap2;
}
void testautoptr2()
{
	heqing::auto_ptr<int> ap1(new int);
	if (true)
	{
		heqing::auto_ptr<int> ap2(ap1);
		*ap2 = 20;//出了函数作用域,ap2释放资源
	}
	*ap1 = 10;//ap1并不知道,所以会导致野指针
}
int main()
{
	testautoptr();
	return 0;
}

上面实现的这个版本的本质是通过管理权限的转移来解决浅拷贝的问题。有可能带来一个比较大的问题:可能会导致野指针。这样的设计在原本就有问题。例子如上。因此,C++98最终使用的还是资源转移来进行实现的。但还是建议在什么情况下都不要使用auto_ptr。

unique_ptr(C++11)
浅拷贝引起的原因是因为默认拷贝构造函数和默认赋值运算符重载。
而unique_ptr提出的解决浅拷贝的方法就是资源独占。不让使用默认的拷贝构造函数和默认的赋值运算符重载。(缺点:应用场景受限)

解决方法:
1,把拷贝构造函数和赋值运算符重载的操作只进行声明,不实现。并将其设为私有。防止用户自己实现。
2,使用=delete将默认的这两个函数删除。

简单的实现unique_ptr:

namespace heqing
{
	//解决浅拷贝的方式就是资源独占(只能一个对象使用,不能共享),就是禁止调用拷贝构造和赋值运算符重载
	template < class T >
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				delete _ptr;//缺点:只能处理new出来的资源,不能处理任意类型的资源
				_ptr = nullptr;
			}
		}
		T& operator*()
		{
			retirn *_pre;
		}
		T* operator->()
		{
			return _ptr;
		}
		//解决浅拷贝
		//C++98的解决方法
	//private://防止用户在外部实现
	//	unique_ptr(const unique_ptr<T>& up);
	//	unique_ptr<T>& operator=(const unique_ptr<T> &up);
		//C++11的解决方法
		unique_ptr(const unique_ptr<T>& up) = delete;//删除该默认成员函数
		unique_ptr<T>& operator=(const unique_ptr<T> &up) = delete;
	private:
		T* _ptr;
	};
}
void TestUniqueptr()
{
	heqing::unique_ptr<int> up1(new int);
	//heqing::unique_ptr<int> up2(up1);
	heqing::unique_ptr<int> up3 (new int);
	up3 = up1;
}
int main()
{
	TestUniqueptr();
	return 0;
}

shared_ptr
shared_ptr是通过采用引用计数的方式解决浅拷贝问题
优势:多个对象之间共享内存
劣势:可能存在循环引用,最终造成资源泄漏
模拟实现shared_ptr

namespace heqing
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(nullptr)
		{
			if (_ptr)
			{
				_pcount = new int(1);
			}
		}
		~shared_ptr()
		{
			if (_ptr && 0 == --*_pcount)
			{
				delete _ptr;
				delete _pcount;
			}
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			if (_ptr)
			{
				++*_pcount;
			}
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (this != &sp)
			{
				//1,与就资源断开联系,保证当前资源只有this一个对象在控制,释放该资源
				if (_ptr && 0 == --*_pcount)
				{
					delete _ptr;
					delete _pcount;
				}
				//2.与sp共享资源和计数
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				if (_ptr)
				{
					++*_pcount;
				}
			}
			return *this;
		}
		int use_count()
		{
			return *_pcount;
		}
	private:
		T* _ptr;
		int* _pcount;
	};
}
void Testsharedptr()
{
	heqing::shared_ptr<int> sp1(new int);
	cout << sp1.use_count() << endl;

	heqing::shared_ptr<int> sp2(sp1);
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	heqing::shared_ptr<int> sp3(new int);
	cout << sp3.use_count() << endl;

}
int main()
{
	Testsharedptr();
	system("pause");
	return 0;
}

模拟实现的shared_ptr有一个最大的安全隐患,它是线程不安全的。因此作出了以下的改进

#include<mutex>
template<class T>
class DFDef
{
public:
	void operator()(T*& ptr)
	{
		if (ptr)
		{
			delete ptr;
			ptr = nullptr;
		}
	}
};

namespace heqing
{
	template<class T,class DF=DFDef<T>>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(nullptr)
			, _pMutex(nullptr)
		{
			if (_ptr)
			{
				_pcount = new int(1);
				_pMutex = new mutex;
			}
		}
		~shared_ptr()
		{
			Release();
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pMutex(sp._pMutex)
		{
			AddRef();
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (this != &sp)
			{
				Release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				if (_ptr)
				{
					AddRef();
				}
			}
			return *this;
		}
		int use_count()
		{
			return *_pcount;
		}
	private:
		void AddRef()
		{
			if (_pcount)
			{
				_pMutex->lock();
				++*_pcount;
				_pMutex->unlock();
			}
		}
		int SubRef()
		{
			if (_pcount)
			{
				_pMutex->lock();
				--*_pcount;
				_pMutex->unlock();
			}
			return *_pcount;
		}
		void Release()
		{
			if (_ptr && 0 == SubRef())
			{
				DF()(_ptr);
				delete _pcount;
			}
		}
		T* _ptr;
		int* _pcount;
		mutex* _pMutex;
	};
}

shared_ptr可能造成循环引用的问题
先看下面这段代码

#include <memory>
struct ListNode
{
	ListNode(int data = 0)
		: pre(nullptr)
		, next(nullptr)
		, _data(data)
	{
		cout << "ListNode(int):" << this << endl;
	}
	~ListNode()
	{
		cout << "~ListNode():" << this << endl;
	}

	shared_ptr<ListNode> pre;
	shared_ptr<ListNode> next;
	int _data;
};

void TestListNode()
{
	shared_ptr<ListNode> sp1(new ListNode(10));
	shared_ptr<ListNode> sp2(new ListNode(20));
	sp1->next = sp2;
	sp2->pre = sp1;
}

int main()
{
	TestListNode();
	return 0;
}

在这里插入图片描述
从上图中可以看到无限循环的问题。当我们想要删除其中的某一个节点时,就会造成无限循环的问题。例如我们要删除sp1这个节点,我们需要把_pcount的值置为1才能进行删除,那我们就需要删除sp2->pre这个节点,而我们要删除这个节点就需要销毁sp2。如果要销毁sp2,就需要把sp2的_pcount置为1,那就需要删除sp1的next。这样一来就陷入了无限循环中。这样就导致两个对象的引用计数_pcount都无法达到0,最终都无法释放堆上的资源,从而导致内存泄漏。

解决循环引用的问题
使用weak_ptr来解决shared_ptr中存在的循环引用的问题。weak_ptr的实现原理和shared_ptr类似,都是通过引用计数的方式。weak_ptr不能独立的管理资源,因为weak_ptr必须配合shared_ptr。

struct ListNode
{
	ListNode(int data = 0)
	: _data(data)
	{
		cout << "ListNode(int):" << this << endl;
	}

	~ListNode()
	{
		cout << "~ListNode():" << this << endl;
	}
	weak_ptr<ListNode> pre;
	weak_ptr<ListNode> next;

	int _data;
};

void TestListNode()
{
	shared_ptr<ListNode> sp1(new ListNode(10));
	shared_ptr<ListNode> sp2(new ListNode(20));
	sp1->next = sp2;
	sp2->pre = sp1;
}

int main()
{
	// weak_ptr<int> sp1;  // 可以编译成功
	//weak_ptr<int> sp2(new int);   // 编译失败--原因:weak_ptr不能独立管理资源
	TestListNode();
	return 0;
}

智能指针的应用场景
1,在任何情况下都尽量不要使用auto_ptr
2,如果不需要对象资源共享------unique_ptr
3,需要对象资源共享---------------shared_ptr
4,出现循环引用是采用weak_ptr

猜你喜欢

转载自blog.csdn.net/weixin_44930562/article/details/103326889