c++中的智能指针详解(RAII, auto_ptr的原理及其改进,unique_ptr的原理,shared_ptr的原理,shared_ptr的缺陷及其改进)

为什么需要智能指针

代码中途退出,也能保证资源的合理释放,在c++中没有垃圾回收机制的情况下,智能指针就可以保证我们申请的资源,最后忘记释放的问题,防止内存泄露,也帮我们减少了一定的负担,不用再在所有可能退出的地方都进行是否释放的检测。

RAII

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

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

利用RAII的思想创建一个smartptr类替我们管理资源

template<class T>

class smartptr
{
public:
	smartptr( T* ptr = nullptr)
		:_ptr(ptr)
	{

	}

	~smartptr()
	{
		if (_ptr)
		{
			delete _ptr;
		}
	}
private:
	T *_ptr;
};

void TestSmartPtr()
{
	smartptr<int>sp(new int);
	throw 1;
}

int  main()
{
	try{
		TestSmartPtr();
	}
	catch (int err)
	{
		cout << err << endl;
	}
	system("pause");
	return 0;
}

这个类虽然能利用自身的生命周期帮助我们管理资源的释放,但它本身还不能称作为一个智能指针,因为它并不能解引用,所以,我们要重载解引用运算符*和箭头运算符->

T& operator*() //重载解引用运算符
	{
		return *_ptr; //返回当前内容
	}
T* operator->()
	{
		return &(operator*()); //把指针所指向空间的地址返回
	}
struct A
{
	int a;
	int b;
	int c;
};
void TestSmartPtr()
{
	smartptr<int>sp1(new int);
	*sp1 = 100;
	cout << *sp1 << endl;

	smartptr<A>sp2(new A);//我们自己的这个类,也可以用指针的方式访问数据
	sp2->a = 10;
	sp2->b = 20;
	sp2->c = 30;

	throw 1;
}

这基本就是一个简单的智能指针,但是这个“指针”有一个巨大的缺陷,它并没有处理浅拷贝问题

smartptr<A>sp3(sp2);

在这里插入图片描述
sp3和sp2他们管理的资源都是一样的,但是我们这里并不能用深拷贝的方式来解决这个问题,因为资源是外部的用户提供的,本身写的类并没有申请空间的权力

智能指针基本原理

RAII(资源可以自动释放)+operator*()/operator->()(当成指针使用)+解决浅拷贝

C++98提供的 auto_ptr

RAII(资源可以自动释放)+operator*()/operator->()(当成指针使用)+资源管理权限的转移

资源管理权限的转移的图解

c++中的智能指针详解(RAII, auto_ptr的原理及其改进,unique_ptr的原理,shared_ptr的原理,shared_ptr的缺陷及其改进)_第1张图片

namespace bite  //自己写一个命名空间,为了防止和系统冲突
{
	//auto_ptr 利用资源管理权限的转移来解决浅拷贝
	template<class T>
	class auto_ptr
	{
		//RALL
	public:
		auto_ptr(T *_ptr = nullptr)
			:_ptr(_ptr)
		{

		}
		//资源的转移
		auto_ptr(auto_ptr<T>& ap) //要修改内容,所以不用加const
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
		~auto_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
			}
		}

		//像指针一样的方式使用
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			//将指针所指向空间的地址返回就可以
			return &(operator*());
		}
	protected:
		T *_ptr;
	};
}

void TestAuto_ptr()
{
	bite::auto_ptr<int>ap1(new int);
	bite::auto_ptr<int>ap2(ap1);
}

在这里插入图片描述
此时我们可以看到,ap1和ap2已经不再指向同一块内存空间了,但是赋值操作也是一个浅拷贝,所以我们还要重载赋值运算符

auto_ptr<T>& operator=(auto_ptr<T>& ap)
	{
		if (this != &ap){  //是不是自己给自己赋值

			if (_ptr)     //当前对象有没有管理资源
				delete _ptr;  //如果有,释放

			_ptr = ap._ptr;  //当前对象接受ap的资源
			ap._ptr = nullptr; //ap 指空
		}
		return *this;
	}

c++中的智能指针详解(RAII, auto_ptr的原理及其改进,unique_ptr的原理,shared_ptr的原理,shared_ptr的缺陷及其改进)_第2张图片

缺陷

因为auto_ptr用的是资源的转移,所以不能同时对两个对象进行操作,就像下面这样
c++中的智能指针详解(RAII, auto_ptr的原理及其改进,unique_ptr的原理,shared_ptr的原理,shared_ptr的缺陷及其改进)_第3张图片

改进后的auto_ptr

//改进:资源管理权转移
namespace bite  //自己写一个命名空间,为了防止和系统冲突
{
	//auto_ptr 利用资源管理权限的转移来解决浅拷贝
	template<class T>
	class auto_ptr
	{
		//RAII
	public:
		auto_ptr(T *_ptr = nullptr)
			:_ptr(_ptr)
			, _owner(false)
		{
			if (_ptr)
				delete _ptr;
		}
		//资源的转移
		auto_ptr(auto_ptr<T>& ap) //要修改内容,所以不用加const
			:_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;  //当前对象接受ap的资源
				_owner = ap._owner;
				ap._owner = false;
			}
			return *this;
		}

		~auto_ptr()
		{
			if (_ptr && _owner)
			{
				delete _ptr;
				_owner = false;
			}
		}

		//像指针一样的方式使用
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			//将指针所指向空间的地址返回就可以
			return &(operator*());
		}

	protected:
		T *_ptr;
		bool _owner; //资源真正的管理权限
	};
}

void TestAuto_ptr()
{


	bite::auto_ptr<int>ap1(new int);
	bite::auto_ptr<int>ap2(ap1);

	ap1 = ap2;
}

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

但是这个改进后又有一个更大的缺陷???,就是造成野指针,所以还是尽量不要用这个智能指针

c++ 11 unique_ptr

一个对象独占资源

实现原理

  1. unique_ptr 禁止调用拷贝构造函数
  2. unique_ptr 禁止调用赋值运算符重载
  3. 完成一份资源被一个对象独占
  4. c++98 中的方式:拷贝构造函数以及赋值运算符重载只声明不定义,访问权限设置为private,为了防止友员,不能定义,只能声明
  5. c++11 中的方式:通过delet 禁止编译器生成默认成员函数
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;

实现

namespace bite
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T *ptr = nullptr)
			:_ptr(ptr)
		{

		}
		~unique_ptr()
		{
			if (_ptr)  //如果提供资源
				delete _ptr; //释放掉资源
		}


		//具有指针的行为
		T &operator*()
		{
			return *_ptr;
		}

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

		///c++98:拷贝构造函数以及赋值运算符重载只声明不定义,访问权限设置为private
		//如果定义,如果用户把函数当成这个类的友员函数的话,私有权限就不复存在
	/*private:
		unique_ptr(const unique_ptr&);
		unique_ptr& operator=(const unique_ptr&)*/


		//c++11:将默认成员函数删除掉
		//delete:用来释放new申请出来的空间
		//       禁止编译器生成默认的成员函数

		unique_ptr(const unique_ptr<T>&) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
	private:
		T * _ptr;

	};
}

c++ 11:shared_ptr

共享资源
RALL+operator()/operator->()+引用计数*

实现原理

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

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

模拟实现

namespace bite
{
	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)
		{
			++ *_pCount;
		}

		//sp2 = sp1
		//sp2 自己是否有资源---->没有----->直接共享
		//                       有 1.资源的计数是1   2.计数>1(多个资源共享)
		shared_ptr<T>& operator=(const shared_ptr<T>&sp)
		{
			if (this != &sp)  //如果不是自己给自己赋值
			{

				//将this从资源中分离开
				if (_ptr  && 0 == --*_pCount)
				{
					delete _ptr;
					delete _pCount;
				}

				//共享资源
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				++*_pCount;
			}

			return *this;
		}
	private:
		T *_ptr;
		int *_pCount;
	};
}

void TestSharedPtr()
{
	bite::shared_ptr<int >sp1(new int);
	bite::shared_ptr<int>sp2(sp1);

	//当前对象没有资源
	bite::shared_ptr<int>sp3;
	sp3 = sp2;

	//当前对象独立拥有资源
	bite::shared_ptr<int>sp4(new int);
	sp4 = sp3;

	bite::shared_ptr<int>sp5(new int);
	bite::shared_ptr<int>sp6(sp5);
	sp5 = sp4;
}

可以看到他们共用一份资源,计数也正常,sp6用的是sp5的资源,计数变成1了。释放的时候从后往前释放

c++中的智能指针详解(RAII, auto_ptr的原理及其改进,unique_ptr的原理,shared_ptr的原理,shared_ptr的缺陷及其改进)_第4张图片

只是我们现在实现的智能指针有一些缺陷

缺陷

  1. 不能管理任意类型的指针,比如:文件指针 析构函数:资源采用delete,而文件需要fclose进行关闭,但我们的析构函数已经写死了
  2. 没有保证线程安全
  3. 存在循环引用----怎么解决?

解决任意类型的指针

定制删除器

//new出来资源的释放
template<class T>
struct DFDel
{
	void operator()(T*& ptr)
	{
		if (ptr){
			delete ptr;
			ptr = nullptr;
		}
	}
};
//malooc出来的资源的释放
template<class T>
struct Free
{
	void operator()(T*& ptr)
	{
		if (ptr){
			free(ptr);
			ptr = nullptr;
		}
	}
};

//处理文件指针的释放
struct Fclose
{
	void operator()(FILE *&pf)
	{
		if (pf)
		{
			fclose(pf);
			pf = nullptr;
		}
	}
};
namespace bite
{
	template < class T, class DF = DFDel<T>>

	class shared_ptr
	{
	public:
		shared_ptr(T *ptr = nullptr)
			:_ptr(ptr)
			, _pCount(nullptr)
		{
			if (_ptr){
				_pCount = new int(1);
			}
		}

		~shared_ptr()
		{
			Release();
		}

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

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

		//采用引用计数的方式解决浅拷贝的问题
		shared_ptr(const shared_ptr<T>&sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
		{
			++ *_pCount;
		}

		//sp2 = sp1
		//sp2 自己是否有资源---->没有----->直接共享
		//                       有 1.资源的计数是1   2.计数>1(多个资源共享)
		shared_ptr<T>& operator=(const shared_ptr<T>&sp)
		{
			if (this != &sp)  //如果不是自己给自己赋值
			{

				//将this从资源中分离开
				if (_ptr && 0 == --*_pCount)
				{
					Release();
				}

				//共享资源
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				++*_pCount;
			}

			return *this;
		}

	private:
		void Release()
		{
			if (_ptr && 0 == --*_pCount)
			{
				//delete _ptr;
				DF()(_ptr);  //无名对象
				delete _pCount;
			}
		}
	private:
		T *_ptr;
		int *_pCount;
	};
}

void TestSharedPtr()
{
	bite::shared_ptr<int>sp1(new int);
	bite::shared_ptr<int,Free<int>>sp2((int *)malloc(sizeof(int)));
	bite::shared_ptr<FILE,Fclose>sp3(fopen(("List.h"), "r"));
}

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

解决线程安全

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

我们需要加锁解决

#include
namespace bite
{
	template < class T, class DF = DFDel<T>>

	class shared_ptr
	{
	public:
		shared_ptr(T *ptr = nullptr)
			:_ptr(ptr)
			, _pCount(nullptr)
			, _pMutex(nullptr)
		{
			if (_ptr){
				_pMutex = new mutex;
				_pCount = new int(1);
			}
		}

		~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();
		}

		//sp2 = sp1
		//sp2 自己是否有资源---->没有----->直接共享
		//                       有 1.资源的计数是1   2.计数>1(多个资源共享)
		shared_ptr<T>& operator=(const shared_ptr<T>&sp)
		{
			if (this != &sp)  //如果不是自己给自己赋值
			{

				//将this从资源中分离开
				if (_ptr && 0 == SubRef())
				{
					Release();
				}

				//共享资源
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				AddRef();
			}

			return *this;
		}

		size_t use_count()const
		{
			return *_pCount;
		}

		T *get()
		{
			return _ptr;  //返回地址
		}
	private:
		void Release()
		{
			if (_ptr && 0 == SubRef())
			{
				//delete _ptr;
				DF()(_ptr);  //无名对象
				delete _pCount;
				if (_pMutex)
					delete _pMutex;
			}
		}

		void AddRef()   //引用计数变更时需要进行加锁和解锁
		{
			_pMutex->lock();
			++*_pCount;
			_pMutex->unlock();
		}

		int SubRef()
		{
			_pMutex->lock();
			--*_pCount;
			_pMutex->unlock();
			return *_pCount;
		}
	private:
		T *_ptr;
		int *_pCount;
		mutex* _pMutex;
	};
}

加的锁可以保证shared_ptr是安全的,但无法保证shared_ptr所指向的内容是不是安全的,如果想要保证,必须用户自己来进行保证

解决循环引用

什么是循环引用

我们来看这个代码

struct ListNode
{
public:
	ListNode(int data)
		:_data(data)
		, _pPre(nullptr)
		, _pNext(nullptr)
	{
		cout << "ListNode(int)" << this << endl;
	}

	~ListNode()
	{
		cout << "~ListNode(int):" << this << endl;
	}

	int _data;
	shared_ptr<ListNode>_pPre;
	shared_ptr<ListNode>_pNext;
	//ListNode * _pPre;
	//ListNode * _pNext;
};
void TestSharedPtr()
{
	shared_ptr<ListNode>sp1(new ListNode(10));
	shared_ptr<ListNode>sp2(new ListNode(20));

	//查看引用计数
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	sp1->_pNext = sp2;
	sp2->_pPre = sp1;
	//查看引用计数
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl; 
}

c++中的智能指针详解(RAII, auto_ptr的原理及其改进,unique_ptr的原理,shared_ptr的原理,shared_ptr的缺陷及其改进)_第5张图片

解决方法

weak_ptr

标准库引入了新的智能指针weak_ptr:RAII+operator*()/operator->()+引用计数,原理跟shared_ptr一样

  • 作用
    专门解决shared_ptr存在得循环引用问题,除此之外,没有其他别得用途
  • 限制
    不能单独管理资源,必须与shared_ptr配合使用

所以

  1. 解决方案:在引用计数的场景下,把结点中的_prev_next改成weak_ptr就可以了
  2. 原理就是,node1->_next = node2;node2->_prev = node1;weak_ptr_next_prev不会增加 node1node2的引用计数。
struct ListNode
{
public:
	ListNode(int data)
		:_data(data)
		//, _pPre(nullptr)
		//, _pNext(nullptr)
	{
		cout << "ListNode(int)" << this << endl;
	}

	~ListNode()
	{
		cout << "~ListNode(int):" << this << endl;
	}

	int _data;
	weak_ptr<ListNode>_pPre;
	weak_ptr<ListNode>_pNext;
	//ListNode * _pPre;
	//ListNode * _pNext;
};

void TestSharedPtr()
{
	shared_ptr<ListNode>sp1(new ListNode(10));
	shared_ptr<ListNode>sp2(new ListNode(20));
	

	//查看引用计数
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	sp1->_pNext = sp2;
	sp2->_pPre = sp1;
	//查看引用计数
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl; 
}
int main()
{
	
	TestSharedPtr();
	return 0;
}

shared_ptr 原型

在编译器中,shared_ptr 就是根据下图的方式定义
c++中的智能指针详解(RAII, auto_ptr的原理及其改进,unique_ptr的原理,shared_ptr的原理,shared_ptr的缺陷及其改进)_第6张图片

C++11和boost中智能指针的关系

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的 scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

RAII解决死锁

RAII思想除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题
c++中的智能指针详解(RAII, auto_ptr的原理及其改进,unique_ptr的原理,shared_ptr的原理,shared_ptr的缺陷及其改进)_第7张图片

#include  
#include 
// C++11的库中也有一个lock_guard,下面的LockGuard造轮子其实就是为了学习他的原理 template 
class LockGuard 
{
 public:    
 LockGuard(Mutex& mtx)        
 		:_mutex(mtx)    
 	{        
 		_mutex.lock();    
 	}
 
    ~LockGuard()  
    {        
    	_mutex.unlock();    
    } 
    LockGuard(const LockGuard<Mutex>&) = delete;
 
private:    
// 注意这里必须使用引用,否则锁的就不是一个互斥量对象    
	Mutex& _mutex; 
};

mutex mtx; 
static int n = 0;
 
void Func() 
{    
	for (size_t i = 0; i < 1000000; ++i)    
	{        
		LockGuard<mutex> lock(mtx);       
	 	++n;    
	 }
}
int main() 
{    
	int begin = clock();    
	thread  t1(Func);    
	thread  t2(Func);
 
    t1.join();    
    t2.join();
 
    int end = clock();
 
    cout << n << endl;    
    cout <<"cost time:" <<end - begin << endl;       
    return 0; 
} 
 

你可能感兴趣的:(c++中的重点问题,c++学习之路)