智能指针介绍(C++)

前言

        关于智能指针大家或多或少都有听说过,因为在C++中没有GC,所以存在很多内存泄露的风险,所以基于RAII思想设计出了,智能指针,智能指针经过了很多个版本的迭代,从刚开始在C++98中推出了auto_ptr,但是auto_ptr不好用,它的设计存在重大缺陷,又因为C++的官方库更新的很慢,所以在接下来的n年中,没有改进,但是有一群大佬绝大C++库太简陋了,所以就自己搞了一个非官方的社区,写了一个库叫做boost,它里面重新写了智能指针。(boost库里面还有很多的其它东西),scoped_ptr/scoped防拷贝版本。shared_ptr/shared_ptr引用计数版本weak_ptr-为了解决shared_ptr循环引用的问题。之后在C++11中官方引入了智能指针,参考boost的实现,稍微进行改进,其实C++11其它类似于右值引用移动语义等也是参考boost。unique_ptr:防拷贝版本,shared_ptr引用计数版本,weak_ptr解决shared_ptr循环引用的问题

目录

1.内存泄露 

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

        2.1RAII

        2.2智能指针的原理

        2.3std::auto_ptr

        2.4std::uniqueptr 

        2.5shared_ptr

3.全部代码


1.内存泄露 

        详见内存泄露的危害 

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

        2.1RAII

        RAII(Resource Acquisition Is Initialization)是一种利用 对象的声明周期来管理资源(如内存,文件句柄,网络连接,互斥量等等)的简单技术。

        在对象构造的时候获取资源,接着控制对资源的访问使之在对象的声明周期内始终保持有效,最后在对象生命周期结束之后通过调用析构函数来对资源进行释放。借此,我们实际上把管理资源的任务托管给了一个对象。这样做的好处有两个:

        1.不需要显示的释放资源

        2.采用这种方式,对象所需要的资源在其生命周期内始终有效。

namespace qyy
{
    //使用RAII思想设计的SmartPtr类
	template
	class SmartPtr
	{
	public:
		SmartPtr(T*ptr)
			:_ptr(ptr)
		{}
		~SmartPtr()
		{
			delete _ptr;
			cout << "~SmartPtr()" << endl;
		
	private:
		T *_ptr;
	};
}
void Test()
{
	int* p1 = new int(1);
	qyy::SmartPtr sp1(p1);
	int* p2 = new int(8);
	qyy::SmartPtr sp2(p2);
	
}

        2.2智能指针的原理

        

但是上述的SmartPtr还不能称为智能指针,因为它还不具备指针的行为,指针可以解引用,也可以通过->去访问空间中内容,因此:AutoPtr模本类中还需要将*,->进行重载,才可以像指针一样去使用。

template
	class SmartPtr
	{
	public:
		SmartPtr(T*ptr)
			:_ptr(ptr)
		{}
		T& operator*()
		{
			return *(_ptr);
		}
        //重载*和->
		T* operator->()
		{
			return _ptr;
		}
		~SmartPtr()
		{
			delete _ptr;
			cout << "~SmartPtr()" << endl;
		}
	
	private:
		T *_ptr;
	};
struct Date
{
	Date(int year = 1,int month = 1,int day = 0)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	int _year;
	int _month;
	int _day;
};
void TestSmartPtr()
{
	int* p1 = new int(1);
	qyy::SmartPtr sp1(p1);
	cout << *sp1 << endl;//像指针一样使用
	qyy::SmartPtrdate(new Date);
	//需要注意的是这里应该是date.operator->()->year = 1;
	//本来应该是date->->year这里语法上为了可读性省略了一个->
	cout << date->_month << endl;
	cout << date->_day << endl;
}

        总的来说:智能指针的原理就是:利用RAII特性并且实现指针一样的行为 

        2.3std::auto_ptr

        std::auto_ptr介绍文档

        在C++98中就实现了auto_ptr的智能指针, 它采用管理权转移的思想,下面简化模拟实现了一

份qyy::auto_ptr来了解它的原理。

       

    //简化版本
    //C++98管理权转移思想
    template
	class auto_ptr
	{
	public:
		auto_ptr(T*ptr)
			:_ptr(ptr)
		{}
		//管理权转移
		//拷贝构造的管理权转移
		auto_ptr(auto_ptr& ap)
			:_ptr(nullptr)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		//operator=的管理权转移
		auto_ptr& operator=(auto_ptr& ap)
		{
			delete _ptr;
			_ptr = nullptr;
			_ptr = ap._ptr;
			ap._ptr = nullptr;
			return *this;
		}
		T&operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		~auto_ptr()
		{
			if(_ptr)//确保指针不为空
				delete _ptr;
		}
	private:
		T* _ptr;
	};

         //测试代码

        

void Test1()
{
	qyy::auto_ptr p1(new int(90));
	qyy::auto_ptr p2(p1);
	qyy::auto_ptr p3(new int(20));
	p3 = p2;
	//进行控制权转移,缺陷原来的智能指针无法使用存在坑
	/**p1 = 10;
	cout << *p1 << endl;*/
}

        由于auto_ptr的设计存在缺陷,如果使用它进行拷贝构造和赋值,原来的智能指针就不能用了,很明显这是存在很大的坑的,所以不太推荐使用。

        2.4std::uniqueptr 

        C++11开始提供更靠谱的unique_ptr。 

        std::unique_ptr的介绍文档 

        unique_ptr的实现原理:简单粗暴的防拷贝,下面通过模拟实现的简化版本的unique_ptr来了解它的原理。 

        

    //unique_ptr的简化版本
    template
	class unique_ptr
	{
	public:
		unique_ptr(T*ptr)
			:_ptr(ptr)
		{}
		unique_ptr(unique_ptr& up) = delete;
		unique_ptr operator=(unique_ptr& up) = delete;
		//重载operator*和operator->
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		~unique_ptr()
		{
			delete _ptr;
		}
	private:
		T* _ptr;
	};

        //测试代码

        

void Test2()
{
	qyy::unique_ptr p1 (new int(30));
	//qyy::unique_ptr p2(p1);
	qyy::unique_ptr p2(new int(40));
	//p1 = p2;
	//缺陷是无法拷贝构造和赋值
}

        unique_ptr的缺陷就是无法拷贝构造和赋值

        2.5shared_ptr

        C++11还提供了一种可以进行拷贝构造和赋值的智能指针shared_ptr;

         std::shared_ptr介绍文档

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

        1.shared_ptr在其内部,给每个资源都维护着一份引用计数,用来记录该份资源被几个对象共享。

        2.在对象被销毁时(也就是析构函数调用的时候),就说明自己不使用该资源了,对象的引用计数减一

        3.如果引用计数是0,说明自己是最后一个使用该资源的对象,必须释放该资源;

        4.如果不是0,就说明除了自己还有别的人也在使用该资源,不能释放该资源,否则其他对象就成野指针了。 

//引用计数支持多个资源拷贝管理同一个资源,最后一个析构对象释放资源
	template
	class shared_ptr
	{
	public:
		//构造函数,将资源交给对象管理
		shared_ptr(T*ptr)
			:_ptr(ptr)
			,_pCount(new int(1))
			,_pMutex(new mutex)
		{}
		//析构函数,对资源进行清理
		~shared_ptr()
		{
			/*if (-- * _count == 0)
			{
				delete _ptr;
				delete _count;
			}*/
			release();
		}
		//拷贝构造,完成多个对象的资源共享
		shared_ptr(shared_ptr& sp)
		{
			_ptr = sp._ptr;
			_pCount = sp._pCount;
			_pMutex = sp._pMutex;
			//增加引用计数
			/*++*_count;*/
			AddRef();
		}
		shared_ptr& operator=(shared_ptr& sp)
		{
			if (this != &sp)
			{
				
				release();
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_pMutex = sp._pMutex;
				AddRef();
			}
			return *this;
		}
		//避免线程不安全
		void release()
		{
			bool flag = false;
			_pMutex->lock();
			if (-- * _pCount == 0)//需要将之前所管理的资源进行释放
			{
				delete _ptr;
				delete _pCount;
				flag = true;
			}
			_pMutex->unlock();
			if (flag)
				delete _pMutex;
		}
		//避免出现线程安全问题
		void AddRef()
		{
			_pMutex->lock();//加锁
			++(*_pCount);
			_pMutex->unlock();//解锁
		}

		//重载operator*和operator->

		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pCount;//引用计数
		mutex* _pMutex;//锁
	};

        测试代码:

void Test3()
{
	qyy::shared_ptr sp1(new int(10));
	qyy::shared_ptr sp2(sp1);
	qyy::shared_ptr > sp3(new pair(1, 1) );
	cout << *sp1 << endl;
	cout << sp3->first << ":" << sp3->second << endl;
	//解决了防拷贝的缺点
	qyy::shared_ptr sp4(new int(13));
	qyy::shared_ptr sp5(new int(44));
	sp4 = sp5;
	//引用计数是线程安全的吗?
}

          shared_ptr是线程安全的吗?

        是的,引用计数的加减是加锁保护的。但是指向的资源不是线程安全的。指向堆空间的资源的线程安全问题是访问的人处理的,智能指针不管,也管不到。引用计数的线程安全问题是智能指针要处理的。

        shared_ptr的线程安全问题

        下面我们通过程序来测试shared的线程安全问题。需要注意的是shared_ptr的线程安全分为两个方面:

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

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

// 演示引用计数线程安全问题,就把AddRefCount和SubRefCount中的锁去掉
//线程安全是偶发性问题,main函数的n改的大一些概率就会变大
//使用qyy::shared_ptr进行测试,为了方便验证引用计数的线程安全问题,可以使用官方库提供的shared_ptr,可以验证库里面的
//shared_ptr,结论是一样的
struct Date1
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
void SharePtrFunc(qyy::shared_ptr&sp,size_t n,mutex&mtu)
{
	cout << sp.Count() << endl;

	for (size_t i = 0; i < n; ++i)
	{
		//这里智能指针拷贝会++计数,析构会--计数,这里是线程安全的
		qyy::shared_ptr copy(sp);
		//这里智能指针管理的资源不是线程安全的,
		{
			//unique_lock lk(mtu);//这里加锁这些资源的访问也就是线程安全的

			copy->_month++;
			copy->_year++;
		}
		//所以这些值虽然最终在主函数中调用时加了2n次,但是最终结果看到的并不是2n次
	}
}
int main()
{
	qyy::shared_ptr p = new Date1;
	const size_t n = 100000;
	mutex mu;
	thread t1(SharePtrFunc, std::ref(p), n, std::ref(mu));
	thread t2(SharePtrFunc, std::ref(p), n, std::ref(mu));
	t1.join();
	t2.join();
	cout <<"day::" << p->_day << endl;
	cout << "month::"<_month << endl;
	cout << "year::" << p->_year << endl;
	cout << p.Count() << endl;
	return 0;
}

   智能指针介绍(C++)_第1张图片

        std::shared_ptr的循环引用

struct ListNode
{
	int _data;
	shared_ptr_prev;
	shared_ptr_next;
	
	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};
int main()
{
	qyy::shared_ptr node1(new ListNode);
	qyy::shared_ptr node2(new ListNode);
	cout << node1.Count() << endl;
	cout << node2.Count() << endl;

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

	cout << node1.Count() << endl;
	cout << node2.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就释放了,_prev析构了,node1就释放了。

        5.但是_next属于node成员,node1释放了_next才会释放,_prev属于node成员,node2释放了,_prev才会释放。现在是node1和node2谁都无法先释放,它们互相影响。所以这叫循环引用。谁都不会释放。

        因为只有当引用计数变为0时,申请的空间才会被释放掉,但是这里调用析构函数之后两块空间的引用计数都是1,并且node1中的_next指针管理着,node2的空间,node2的_prev指针管理着node1的空间,它们的空间都没有销毁所以空间里面的成员变量都没有调用析构函数,就会造成这样的问题。

        这里有一些抽象需要花一点时间来理解。

智能指针介绍(C++)_第2张图片

        解决方法:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。

        原理就是node1->_next = node2;和node2->_prev = node1;时,weak_ptr的next和_prev不会增加node1和node2的引用计数。因为weak_ptr不会增加引用计数。

        weak_ptr的实现: 

         

    template
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr &sp)
			:_ptr(sp.get_ptr())
		{}
		weak_ptr & operator= (const shared_ptr& sp)
		{
			_ptr = sp.get_ptr();
			return *this;
		}
		T&operator*()
		{
			return _ptr;
		}
		T* operator->()
		{
			return &_ptr;
		}
		
	private:
		T* _ptr;
	};

        严格来说,weak_ptr不是智能指针,因为它不符合RAII的思想,它只是为了解决shared_ptr循环引用中的问题而设计出来的。weak_ptr不会增加引用计数。 

3.全部代码

        //SmartPtr.hpp

#include
#include
using namespace std;
namespace qyy
{
	template
	class SmartPtr
	{
	public:
		SmartPtr(T*ptr)
			:_ptr(ptr)
		{}
		T& operator*()
		{
			return *(_ptr);
		}
		T* operator->()
		{
			return _ptr;
		}
		~SmartPtr()
		{
			delete _ptr;
			cout << "~SmartPtr()" << endl;
		}
		//指针无法拷贝和赋值
		//所以要禁用掉
		SmartPtr&operator=(const SmartPtr& sp) = delete;
		SmartPtr(const SmartPtr& sp) = delete;
	private:
		T *_ptr;
	};
	template
	class auto_ptr
	{
	public:
		auto_ptr(T*ptr)
			:_ptr(ptr)
		{}
		//管理权转移
		//拷贝构造的管理权转移
		auto_ptr(auto_ptr& ap)
			:_ptr(nullptr)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		//operator=的管理权转移
		auto_ptr& operator=(auto_ptr& ap)
		{
			delete _ptr;
			_ptr = nullptr;
			_ptr = ap._ptr;
			ap._ptr = nullptr;
			return *this;
		}
		T&operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		~auto_ptr()
		{
			if(_ptr)//确保指针不为空
				delete _ptr;
		}
	private:
		T* _ptr;
	};
	template
	class unique_ptr
	{
	public:
		unique_ptr(T*ptr)
			:_ptr(ptr)
		{}
		unique_ptr(unique_ptr& up) = delete;
		unique_ptr operator=(unique_ptr& up) = delete;
		//重载operator*和operator->
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		~unique_ptr()
		{
			delete _ptr;
		}
	private:
		T* _ptr;
	};

	//引用计数支持多个资源拷贝管理同一个资源,最后一个析构对象释放资源
	template
	class shared_ptr
	{
	public:
		//构造函数,将资源交给对象管理
		shared_ptr(T*ptr = nullptr)
			:_ptr(ptr)
			,_pCount(new int(1))
			,_pMutex(new mutex)
		{}
		//析构函数,对资源进行清理
		~shared_ptr()
		{
			/*if (-- * _count == 0)
			{
				delete _ptr;
				delete _count;
			}*/
			release();
		}
		//拷贝构造,完成多个对象的资源共享
		shared_ptr(shared_ptr& sp)
		{
			_ptr = sp._ptr;
			_pCount = sp._pCount;
			_pMutex = sp._pMutex;
			//增加引用计数
			/*++*_count;*/
			AddRef();
		}
		shared_ptr& operator=(shared_ptr& sp)
		{
			if (this != &sp)
			{
				
				release();
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_pMutex = sp._pMutex;
				AddRef();
			}
			return *this;
		}
		int Count()
		{
			return *_pCount;
		}
		//避免线程不安全
		void release()
		{
			bool flag = false;
			_pMutex->lock();
			if (-- * _pCount == 0)//需要将之前所管理的资源进行释放
			{
				delete _ptr;
				delete _pCount;
				flag = true;
			}
			_pMutex->unlock();
			if (flag)
				delete _pMutex;
		}
		//避免出现线程安全问题
		void AddRef()
		{
			_pMutex->lock();//加锁
			++(*_pCount);
			_pMutex->unlock();//解锁
		}
		T* get_ptr()const
		{
			return _ptr;
		}
		
		//重载operator*和operator->

		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pCount;//引用计数
		mutex* _pMutex;//锁
	};
	template
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr &sp)
			:_ptr(sp.get_ptr())
		{}
		weak_ptr & operator= (const shared_ptr& sp)
		{
			_ptr = sp.get_ptr();
			return *this;
		}
		T&operator*()
		{
			return _ptr;
		}
		T* operator->()
		{
			return &_ptr;
		}
		
	private:
		T* _ptr;
	};
}

        //Test.cpp 

#include"SmartPtr.hpp"

struct Date
{
	Date(int year = 1,int month = 1,int day = 0)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	int _year;
	int _month;
	int _day;
};
void TestSmartPtr()
{
	int* p1 = new int(1);
	qyy::SmartPtr sp1(p1);
	cout << *sp1 << endl;//像指针一样使用
	qyy::SmartPtrdate(new Date);
	//需要注意的是这里应该是date.operator->()->year = 1;
	//本来应该是date->->year这里语法上为了可读性省略了一个->
	cout << date->_month << endl;
	cout << date->_day << endl;
}
void Test()
{
	int* p1 = new int(1);
	qyy::SmartPtr sp1(p1);
	int* p2 = new int(8);
	qyy::SmartPtr sp2(p2);
	//sp2 = sp1;
	//缺陷是如果进行智能指针的拷贝构造和赋值程序会崩溃
	//qyy::SmartPtr p2(p1);
	cout << *sp1<< endl;
}
void Test1()
{
	qyy::auto_ptr p1(new int(90));
	qyy::auto_ptr p2(p1);
	qyy::auto_ptr p3(new int(20));
	p3 = p2;
	//进行控制权转移,缺陷原来的智能指针无法使用存在坑
	/**p1 = 10;
	cout << *p1 << endl;*/
}
void Test2()
{
	qyy::unique_ptr p1 (new int(30));
	//qyy::unique_ptr p2(p1);
	qyy::unique_ptr p2(new int(40));
	//p1 = p2;
	//缺陷是无法拷贝构造和赋值
}
void Test3()
{
	qyy::shared_ptr sp1(new int(10));
	qyy::shared_ptr sp2(sp1);
	qyy::shared_ptr > sp3(new pair(1, 1) );
	cout << *sp1 << endl;
	cout << sp3->first << ":" << sp3->second << endl;
	//解决了防拷贝的缺点
	qyy::shared_ptr sp4(new int(13));
	qyy::shared_ptr sp5(new int(44));
	sp4 = sp5;
	//引用计数是线程安全的吗?
}
#include
void Test4()
{
	int n = 10;
	for (int i = 0; i < 100; ++i)
	{
		thread t1([]() { 
			qyy::shared_ptr sp1(new int(10));
			cout << sp1.Count() << endl;
			});
		t1.join();
	}
}
// 演示引用计数线程安全问题,就把AddRefCount和SubRefCount中的锁去掉
//线程安全是偶发性问题,main函数的n改的大一些概率就会变大
//使用qyy::shared_ptr进行测试,为了方便验证引用计数的线程安全问题,可以使用官方库提供的shared_ptr,可以验证库里面的
//shared_ptr,结论是一样的
struct Date1
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
void SharePtrFunc(qyy::shared_ptr&sp,size_t n,mutex&mtu)
{
	cout << sp.Count() << endl;

	for (size_t i = 0; i < n; ++i)
	{
		//这里智能指针拷贝会++计数,析构会--计数,这里是线程安全的
		qyy::shared_ptr copy(sp);
		//这里智能指针管理的资源不是线程安全的,
		{
			//unique_lock lk(mtu);//这里加锁这些资源的访问也就是线程安全的
			copy->_day++;
			copy->_month++;
			copy->_year++;
		}
		//所以这些值虽然最终在主函数中调用时加了2n次,但是最终结果看到的并不是2n次
	}
}
int main1()
{
	qyy::shared_ptr p = new Date1;
	const size_t n = 10000000;
	mutex mu;
	thread t1(SharePtrFunc, std::ref(p), n, std::ref(mu));
	thread t2(SharePtrFunc, std::ref(p), n, std::ref(mu));
	t1.join();
	t2.join();
	cout <<"day::" << p->_day << endl;
	cout << "month::"<_month << endl;
	cout << "year::" << p->_year << endl;
	cout << p.Count() << endl;
	return 0;
}
struct ListNode
{
	int _data;
	qyy::weak_ptr_prev;
	qyy::weak_ptr_next;
	
	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};
int main()
{
	qyy::shared_ptr node1(new ListNode);
	qyy::shared_ptr node2(new ListNode);
	cout << node1.Count() << endl;
	cout << node2.Count() << endl;

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

	cout << node1.Count() << endl;
	cout << node2.Count() << endl;
	return 0;
}

你可能感兴趣的:(c++,开发语言)