【C++】C++11之智能指针

为什么C++11要提出智能指针的概念?

  1. new申请内存后,如果触发异常,则就不会处理后面的delete,这样就会导致内存泄露的产生;若要是捕获异常再重新抛出话,代码比较繁杂
  2. 所有的内存申请new与释放delete之间都要捕获,代码繁杂,代码的可读性变得很差

智能指针的使用及其原理

提及智能指针,就必须提到RAII,
RAII是什么呢?
RAII是一种利用对象生命周期控制资源的技术。
对象构造时获取资源,接着控制对资源的访问使它在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。本质上就是把资源托管给一个对象。

智能指针

  1. 将指向动态开辟资源的指针,交给RAII的类管理起来
  2. 像指针一样的使用,支持operator* 和 operator->
  3. 解决拷贝问题

智能指针的发展历史

名称 产生 作用
auto_ptr C++98产生 管理权转移,导致指针悬空
scoped_ptr/scoped_array boost库中的 防拷贝 ,在不需要拷贝的情况下,建议使用它
shared_ptr/weakptr boost库中的 引出引用计数的概念,支持拷贝,拷贝时增加技术,析构时减少计数,引用计数为0时释放资源
unique_ptr C++11产生的 强制的防拷贝,在不需要拷贝的情况下,建议使用它
shared_ptr/weak_ptr C++11产生的 引用计数,支持拷贝,拷贝时增加计数,析构时减少计数 ,引用计数为0时释放资源

C++11中的shared_ptr、weak_ptr就是对boost库中的进行模仿重写,我们这篇就看从auto_ptr到C++11的unque_ptr,再到shared_ptr/weak_ptr的底层实现。

auto_ptr

我们先来实现一个简易版的auto_ptr,来看看它带来的问题

//1.RAII管理资源
//2. 像指针一样使用
namespace lth
{
	template <class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{

		}
		~auto_ptr()
		{
			cout << "delete" << " "<< _ptr << endl;
			delete[] _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}

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

	// auto_ptr的使用
	struct A
	{
		int _a1;
		int _a2;
	};
	//像指针一样使用
	void test_auto_ptr()
	{
		
		auto_ptr<int> ap1(new int);
		auto_ptr<int> ap2(new int);
		*ap1 = 10;
		A* pa = new A;
		pa->_a1 = 20;

		auto_ptr<A> apa(new A);
		(*apa)._a1 = 20;
		apa->_a2 = 100;
	}

	void test_auto_ptr2()
	{
		auto_ptr<int>ap1(new int);
		auto_ptr<int> copy(ap1); // error, 默认的是浅拷贝,内存被管理了两次,最后内存才能也释放了两次		
	}

}

使用auto_ptr的时候的注意事项:

  1. auto_ptr不可以深拷贝。原因是auto_ptr copy(ap1)默认是浅拷贝,内存资源被管理了两次,最后调用析构函数,内存释放了两次。
  2. 那么深拷贝可以吗? 也是不可以的
    1. 资源不是属于智能指针,智能指针只是托管这个资源,并没有权利去深拷贝。
    2. 智能指针,行为也要像指针一样,如果可以支持深拷贝就不像指针了,指针指向的是托管资源,又重新拷贝出一块资源,就不是对原来空间的进行托管的了。

我们知道了auto_ptr存在的问题,但是auto_ptr针对以上的问题,C++98采用的是管理权转移的方法,支持深拷贝。是将现在管理这个资源的类拷贝出新的类之后,将这个类的指针置空实现的。

auto_ptr的完整版模拟实现

	template <class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{

		}
		auto_ptr(auto_ptr<T>& ap)
			: _ptr(ap._ptr)
		{
			ap._ptr = nullptr; // 管理权转移,就把原来管理这块空间的智能指针置空
		}

		//ap1 = ap2;
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				if (_ptr)  // 如果还有资源,先把资源释放掉
				{
					delete _ptr;
				}

				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}

			return *this;
		}
		~auto_ptr()
		{
			cout << "delete" << " " << _ptr << endl;
			delete[] _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}

		T*operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
	void test1()
	{
		auto_ptr<int> ap1 = new int;

		auto_ptr<int> ap = new int[10];
		auto_ptr<int> copy(ap1); //拷贝构造copy1,管理权转移,交给别的类管理


		//这样的问题: 拷贝构造给别人了,转交出去的对象全部都悬空。不能在访问
		*ap1 = 10; // error, 访问空指针,崩溃。 

	}

但是管理权转移也带来了新的问题,我们把auto_ptr copy(ap1);我们把对象ap1深拷贝出对象copy,本来是由ap1管理这个类的,而现在是交给copy对象去管理资源。但是auto_ptr的做法是将ap1这个类管理资源的指针置为空,这样访问ap1这个类就会程序崩溃。

所以C++11又引入了 unique_ptr。

unique_ptr

简单粗暴,不允许拷贝。通过将拷贝构造函数定义为私有实现的。

扫描二维码关注公众号,回复: 11304748 查看本文章

unique_ptr的模拟现实

	template <class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{

		}
		~unique_ptr()
		{
			cout << "delete" << " " << _ptr << endl;
			delete[] _ptr;
		}

		unique_ptr(auto_ptr<T>& ap) = delete;
		unique_ptr<T>& operator=(const auto_ptr<T>& ap) = delete;

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

		T*operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
	void test_unique_ptr()
	{
		unique_ptr<int> up1 = new int();
		unique_ptr<int> copy(up1); //error 不支持拷贝构造

	}

unique_ptr简单粗暴,通过将拷贝构造函数和赋值函数通过delete来禁止掉来实现

shared_ptr

shared_ptr之引用计数

功能:支持拷贝
解决的问题:两个对象或者多个对象管理这块空间的时候,若都调用析构函数,那么这块空间会被释放多次。
实现原理:引入了一个引用计数的概念。
引用计数:记录多少个对象管理着这块资源。管理对象的这个类若要释放调用析构函数的时候要先检查引用计数,如果引用计数是1,则表示最后一个管理者管理着对象,那么就会释放这个资源。否则引用数–。

那么引用计数怎么实现的呢
引用计数属于多个对象的,是静态全局的,所有对象都可以看见。实现方法有两种:

  1. 使用static变量实现引用计数。
  2. 在堆上,为第一个管理资源的智能指针对象中为资源创建一个引用计数。

但是第一种方法有明显的问题,当托管的资源时两个或多个的时候,全局只有一个static变量的引用计数,那么就不知道什么时候一个资源被释放,有可能引用计数还是>0。
比方:

引用计数static int count = 0;
 shared_ptr< int > p1 = new int(); // 引用计数 count++    count=1
 shared_ptr< int > p2 = new int(); // 引用计数 count++    count=2
 shared_ptr< int > p3 = new int(); // 引用计数 count++    count=3
 此时应该每一块空间的引用计数为1, 但是引用计数全都是3。
 这样子的话,当释放p1的时候,应该是p1的引用计数减小到0,但是还是2

所以我们的引用计数应该在堆上建立

但是我们的shared_ptr的引用计数是在堆上的,堆是公共空间,不是线程安全的,所以我们要在对临界资源引用计数修改的时候,要进行加锁。所以我们还要定义互斥锁变量,互斥锁变量也必须是在堆上的。如果不是在堆上的话,互斥锁加锁和解锁操作的就不会是同一个锁。

shared_ptr的模拟实现

	template <class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pCount(new int(1))
			, Mutex(new mutex)
		{
		}
		~shared_ptr()
		{

			ReleaseRef();

		}

		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
			, Mutex(sp.Mutex)
		{
			AddRef();

		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (this != &sp  && _ptr != sp._ptr)
			{

				// 先释放原对象的资源
				ReleaseRef();
				_ptr = sp._ptr;   
				_pCount = sp._pCount; // 引用计数赋值过去
				Mutex = sp.Mutex;  // 互斥锁变量也要赋值过去
				AddRef();
			}
			return *this;
		}


		// 引用计数的线程安全问题
		// 加引用计数
		void AddRef()
		{
			Mutex->lock();
			++(*_pCount);
			Mutex->unlock();
		}

		// 减引用计数
		void ReleaseRef()
		{

			bool deleteflag = false; 
			Mutex->lock();
			if (--(*_pCount) == 0)
			{
				delete _pCount;
				delete _ptr;
				deleteflag = true; //如果资源减到0的话, true
			}
			Mutex->unlock();

			if (deleteflag == true) //释放堆上的锁资源
			{
				delete Mutex;
			}
		}
		T& operator*()
		{
			return *_ptr;
		}

		T*operator->()
		{
			return _ptr;
		}
		int GetRefCount()
		{
			return *_pCount;
		}
		T* GetPtr() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pCount;
		mutex* Mutex;
		
	};
	//测试模块
		void test_shared_ptr()
	{
		shared_ptr<int> sp1 = new int;
		shared_ptr<int> copy1(sp1);
		shared_ptr<int> copy2(copy1); //三个对象管理一块空间,引用计数是3

	}
	//多线程测试模块
	void test_threads()
	{
		shared_ptr<int> sp(new int);
		vector<thread> vthreads;
		for (size_t i = 0; i < 2; ++i)
		{
			vthreads.push_back(
				thread([&]()
			{
				for (int i = 0; i <100000; i++)
				{
					shared_ptr<int> copy(sp);
				}
				
			}
		));
		}
		for (size_t i = 0; i < 2; ++i)
		{
			vthreads[i].join();
		}
		cout << sp.GetRefCount() << endl;
	}

但是shared_ptr也会带来一个问题。循环引用的问题。

循环引用的问题

我们需要弄清楚一下两点问题:

  1. 导致的问题是什么? 内存泄漏
  2. 如何解决的,怎么解决的

循环引用的例子

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

		~ListNode()
		{
			cout << "~ListNode()" << endl;
		}
	};
	void test_cycle_ref()
	{
		shared_ptr<ListNode> node1(new ListNode);
		shared_ptr<ListNode> node2(new ListNode);

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

在这里插入图片描述
两者间形成相互依赖的关系
最初的状态:node1管理这块空间,空间的引用计数为1,node2也是管理一块空间,空间的引用计数为1.
之后: node1的_next也是一个智能指针,指向node2,就相当于node1和node2都管理这node2这块空间,node2的引用计数增加到2,node2的_prev指向node1, 也就是node1和node2也都管理着node1这块空间。所以node1的引用计数也增加到2
释放时:node2,node1都要进行析构,所以node1的空间释放空间,计数-1,现在的引用计数为1, node2的空间的释放,计数-1,引用计数为1。

这时候,两块空间的引用计数都不是0,都是1,所以两块空间的资源没有被释放。 node1节点的释放取决于node2->prev释放,node2->prev释放取决于node2节点的释放,但是node2节点的释放,此时还要取决于node1->next什么时候释放,node1->next什么时候释放,又取决于node1什么时候释放。 所以就是一个死循环,导致两块空间大家的引用计数都不为0,都释放不了这两个空间。

如何解决循环引用

  1. weak_ptr 可以像指针一样去使用,但是他不会增加shared_ptr的引用计数,他不参与对资源的管理。
  2. 可以访问管理的资源,但是不参与对资源的释放。

weak_ptr

简单介绍

  1. weak_ptr 可以像指针一样去使用,但是他不会增加shared_ptr的引用计数,他不参与对资源的管理。
  2. 可以访问管理的资源,但是不参与对资源的释放。

weak_ptr的模拟实现

	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{

		}
		weak_ptr(const shared_ptr<T>& sp) //注意这里参数是shared_ptr,所以要先搭配着上面实现的shared_ptr一起使用
			:_ptr(sp.GetPtr())
		{
		}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)//这里也是shared_ptr
		{
			_ptr = sp.GetPtr(); // shared_ptr类的接口,上面已经实现过了。
			return *this;
		}
		T& operator*()
		{
			return *_ptr;
		}

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

然后搭配着weak_ptr一起这样使用就不会引起循环引用了

	struct ListNode
	{
		weak_ptr<ListNode> _next;
		weak_ptr<ListNode> _prev;
		int val;

		~ListNode()
		{
			cout << "~ListNode()" << endl;
		}
	};
	void test_cycle_ref()
	{
		weak_ptr<ListNode> node1(new ListNode);
		weak_ptr<ListNode> node2(new ListNode);

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

最后的最后,我们来看锁资源管理的概念:

C++11的锁资源管理

lock_guard的模拟实现

作用:管理C++11的锁资源
LockGuard的模拟实现:

template <class Lock>
class LockGuard
{
public:
	LockGuard(Lock&lock)
		:_lock(lock)
	{
		_lock.lock();
	}
	~LockGuard()
	{
		_lock.unlock();
	}

	LockGuard(const LockGuard&) = delete;
private:
	Lock& _lock;
};

多线程加锁的时候可以使用我们锁资源管理这个LockGuard类

int main()
{
	int n = 0;
	//cout << &n << endl;
	mutex mtx;
	vector<thread> vthreads; 
	for (int i = 0; i < 4; i++)
	{
		vthreads.push_back(thread([&n, &mtx]()
		{
			LockGuard<mutex> lg(mtx);
			//lock_guard<mutex> lg(mtx); // C++11 库里实现的
			//unique_lock<mutex> lg(mtx);// C++11 库里的锁资源管理类
			for (size_t i = 0; i < 10000; i++)
			{
				++n;
			}
			
		}
		));
	}
	for (size_t i = 0; i < 4; i++)
	{
		vthreads[i].join();
	}
	cout << n << endl;
	system("pause");
}

问题
C++11的lock_guard和unique_lock的区别与联系?

  1. 都是管理锁的自动释放,解决异常安全问题的RAII类。
  2. unique_lock处理RAII类,还支持手动加锁解锁,比lock_guard多了几个接口。

猜你喜欢

转载自blog.csdn.net/weixin_43939593/article/details/106035216