【C++】智能指针

发布于:2024-05-24 ⋅ 阅读:(40) ⋅ 点赞:(0)


前言

在上一篇异常的文章中我们提及到了利用RAII思想去解决内存泄露问题,而智能指针就是RAII思想的产物,智能指针到底智能在什么地方呢?下面我们一起来学习吧!!

一、为什么需要智能指针

为什么需要智能指针呢?我们来回顾一下异常章节出现的场景:

#include <iostream>
using namespace std;
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = nullptr;
	cout << div() << endl;
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)   // C++库中提供的可以接收任意类型的基类
	{
		cout << e.what() << endl;
	}

	return 0;
}

上述三个问题该如何解决?我们利用重新抛出就能解决上述的问题,那么该如何来写呢?

/*
思路:
	如果p1未申请成功,抛出异常直接被主函数捕获返回主函数;
	如果p1申请成功,而p2未申请成功,则需在异常被主函数捕获前delete p1,再重新抛出异常交给主函数处理。
	此时p1与p2都申请成功,div抛出异常则需在异常被主函数捕获前delete p1 p2,再重新抛出异常交给主函数处理。
*/
#include <iostream>
using namespace std;

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = nullptr;
	try
	{
		p2 = new int;
	}
	catch (...)
	{
		delete p1;
		throw;
	}
	
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		delete p2;
		throw;
	}
	
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)   // C++库中提供的对任意类型接收的基类
	{
		cout << e.what() << endl;
	}

	return 0;
}

我们可以看到问题虽然是解决了,但是设计出的代码确是如此的丑陋,那么对于内存申请与释放问题能不能交给编译器去处理呢?这样就不用我们时时刻刻去操心内存泄露的问题了——智能指针!!

二、智能指针的使用和原理

2.1 RAII思想

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象

这种做法有两大好处:

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

接下来我们基于RAII思想来简单设计一个SmartPtr类,实现对象来管理资源:

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	// div抛异常, 主函数会捕获到异常, 此时sp1与sp2对象出了Func函数作用域会自动调用析构函数完成资源的清理工作
	SmartPtr<int> sp1(new int(1));
	SmartPtr<int> sp2(new int(2));

	cout << div() << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

在这里插入图片描述

2.2 智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr 
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}

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

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

private:
	T* _ptr;
};

智能指针的原理总结: 利用 RAII 思想设计释放资源的类,该类重载了*->,其具有像指针一样的行为。

如果我们想把这个类的功能再丰富一下,比如说让其可以拷贝构造、赋值重载,我们来看看下面的情况:
在这里插入图片描述

我们未重写拷贝构造,那么默认拷贝构造就是浅拷贝,两个对象的成员都指向了同一份资源,那么析构时对同一块空间delete两次就会出现不确定的行为此时程序崩溃,那么按照我们之前所说的浅拷贝不行就换深拷贝分别指向不同块,然后将数据拷贝过来吗?显然在智能指针这里深拷贝是不行的,因为智能指针就需要浅拷贝,使两个对象的成员指向同一份资源!!智能指针拷贝见下图:

在这里插入图片描述

那么如何才能实现智能指针的浅拷贝?使得在析构时不对同一份资源析构两次呢?早期C++库中就提供了几种智能指针的设计方案,下面我们一起来看看它们的设计原理。

2.3 std::auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。auto_ptr的实现原理:管理权转移的思想。

下面我们根据库中的设计简化模拟实现了一份curry::auto_ptr来了解它的原理:

namespace curry
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}

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

		auto_ptr<T>& operator=(const auto_ptr<T>& ap)
		{
			if (_ptr != ap._ptr)  // 不指向同一份资源的话就进行赋值操作
			{
				if (_ptr)  // 释放当前资源
					delete _ptr;

				// 转移ap中的资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}

			return *this;
		}

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

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

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}

	private:
		T* _ptr;
	};

	void test_auto()
	{
		curry::auto_ptr<int> ap1(new int(1));
		curry::auto_ptr<int> ap2(ap1);

		std::auto_ptr<int> ap3(new int(1));
		std::auto_ptr<int> ap4(ap3);

		//*ap1 = 1; // 管理权转移以后导致ap1悬空,不能访问
		//*ap2 = 1;
	}
}

我们拿自己模拟实现的auto_ptr与库中的std::auto_ptr对比一下:

在这里插入图片描述
在这里插入图片描述
从上图我们可以观察到,curry::auto_ptr与std::auto_ptr大致差不多,都是将资源权限主动移交给别人,但是这样造成的后果是移交之后自己悬空了,那么在其他人不知情的情况下对这块空间进行访问就会使程序崩溃!

总结:所以auto_ptr是一个失败设计,很多公司也明确要求不能使用auto_ptr。从原理上来看auto_ptr也不是采用浅拷贝的思想,而更像是把主动把责任推给别人,而且还造成了未知的风险!!

2.4 std::unique_ptr

unique_ptr是一种独占所有权的智能指针,它确保只有一个指针可以拥有资源,当 unique_ptr 被销毁时,它会自动释放所拥有的资源。

unique_ptr的实现原理:简单粗暴的防拷贝。

namespace curry
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}
		
		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "Delete:" << _ptr << endl;
				delete _ptr;
			}
		}

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

		T* operator->()
		{
			return _ptr;
		}
		// C++98的防拷贝做法:将拷贝构造和赋值运算符重载弄成私有,只声明不实现
		// 设置成私有的原因是防止别人在类外实现
		// C++11的防拷贝做法:delete
		unique_ptr(unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;

	private:
		T* _ptr;
	};
}

2.5 std::shared_ptr

shared_ptr 是更靠谱的并且支持拷贝的智能指针。shared_ptr 的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

分析:

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

Q:能否采用全局计数的方式?

不能,假设我们使用静态成员变量来计数的话,所有的对象都共享它,那么不同的对象指向不同的资源该如何来计数呢??所以在每个对象内部都应该维护一份计数并且指向相同资源的对象看到的是同一份计数,因此我们的计数需要采用指针的方式保存!!

shared_ptr的模拟实现:

namespace curry
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pcount(new int(1))
		{}

		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				if (--(*_pcount) == 0 && _ptr)
				{
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}

			return *this;
		}

		~shared_ptr()
		{
			if (--(*_pcount) == 0 && _ptr)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}

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

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

	private:
		T* _ptr;
		int* _pcount;
	};

	void test_shared_ptr()
	{
		curry::shared_ptr<int> sp1(new int(2));
		curry::shared_ptr<int> sp2(sp1);
		curry::shared_ptr<int> sp3(sp2);

		curry::shared_ptr<int> sp4(new int(3));
		sp3 = sp4;

		curry::shared_ptr<int> sp5(new int(1));
		sp5 = sp4;
	}
}

在这里插入图片描述

2.5.1 std::shared_ptr的线程安全问题

shared_ptr的线程安全分为两方面:

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

我们来测试一下第一种情况的场景:

namespace curry
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pcount(new int(1))
		{}

		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				if (--(*_pcount) == 0 && _ptr)
				{
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}

			return *this;
		}

		~shared_ptr()
		{
			if (--(*_pcount) == 0 && _ptr)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}

		T* get()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}

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

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

	private:
		T* _ptr;
		int* _pcount;
	};
	
	struct Date
	{
		int _year = 0;
		int _month = 0;
		int _day = 0;

		~Date()
		{}
	};

	void SharePtrFunc(curry::shared_ptr<Date>& sp, size_t n, mutex& mtx)
	{
		cout << sp.get() << endl;
		// 调用n次拷贝构造看看最终引用计数是否正常  由此来检测此时我们自己模拟的智能指针是否存在线程安全问题
		for (size_t i = 0; i < n; ++i)  
		{
			curry::shared_ptr<Date> copy(sp);
		}
	}

	void test_shared_safe()
	{
		curry::shared_ptr<Date> p(new Date);
		cout << p.get() << endl;

		const size_t n = 1000;
		mutex mtx;
		// 这里不加ref编译是不会通过的, 因为智能指针和锁拷贝不是线程安全的, 要在底层进行之后才能传参
		thread t1(SharePtrFunc, ref(p), n, ref(mtx));
		thread t2(SharePtrFunc, ref(p), n, ref(mtx));

		t1.join();
		t2.join();

		cout << p.use_count() << endl;  // 如果是正常的话, 此时这里应该只剩一个p对象了
	}
}

在这里插入图片描述

我们来看打印结果,当n = 1000时线程貌似是安全的,我们继续调整大小n = 10000:

在这里插入图片描述

因为线程安全问题是偶现性问题,n改大一些概率就变大了,就容易发现问题了,所以此时我们模拟实现的shared_ptr引用计数++ --是线程不安全的,那么我们就要通过加锁的方式保证其安全:

namespace curry
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _pmtx(new mutex)
		{}

		shared_ptr(const shared_ptr<T>& ap)
			: _ptr(ap._ptr)
			, _pcount(ap._pcount)
			, _pmtx(ap._pmtx)
		{
			AddRef();
		}

		void Release()
		{
			_pmtx->lock();
			bool flag = false;
			if (--(*_pcount) == 0 && _ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
				flag = true;
			}

			// 在释放锁之前要释放掉相应的资源
			_pmtx->unlock();
			if (flag == true)
			{
				delete _pmtx;
			}
		}

		void AddRef()
		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}

		shared_ptr<T>& operator=(const shared_ptr& ap)
		{
			if (_ptr != ap._ptr)
			{
				Release();

				_ptr = ap._ptr;
				_pcount = ap._pcount;
				_pmtx = ap._pmtx;

				AddRef();
			}

			return *this;
		}

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

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

		T* get()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}

		~shared_ptr()
		{
			Release();
		}
	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;
	};
}

当此时n的大小为500000足够大时,通过多次检测都为正常现象,由此就保证了线程安全问题,并且库中实现的std::shared_ptr本身也是线程安全的!!

在这里插入图片描述


下面我们来看第二种出现线程安全的场景:

void SharePtrFunc(std::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
	for (size_t i = 0; i < n; ++i)
	{
		std::shared_ptr<Date> copy(sp);

		sp->_year++;
		sp->_day++;
		sp->_month++;
	}
}

void test_shared_safe()
{
	std::shared_ptr<Date> p(new Date);
	cout << p.get() << endl;

	const size_t n = 10000;
	mutex mtx;
	thread t1(SharePtrFunc, ref(p), n, ref(mtx));
	thread t2(SharePtrFunc, ref(p), n, ref(mtx));

	t1.join();
	t2.join();

	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;

	cout << p.use_count() << endl;
}

在这里插入图片描述

通过上图我们发现shared_ptr没有出现线程安全的问题,但是其指向资源不是线程安全的!!智能指针指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了!!所以我们解决的办法也是通过人为的加锁进行处理。

在这里插入图片描述

结论:shared_ptr智能指针是线程安全的,但其指向堆上的资源不是线程安全的,它的访问的人处理的,智能指针不管,也管不了!!

2.5.2 std::shared_ptr的循环引用

下面我们来看一下shared_ptr处理不了的特殊场景:

struct ListNode
{
	int _data;
	curry::shared_ptr<ListNode> _prev;
	curry::shared_ptr<ListNode> _next;

	~ListNode() 
	{ 
		cout << "~ListNode()" << endl; 
	}
};

void test_shared_cycle()
{
	curry::shared_ptr<ListNode> node1(new ListNode);
	curry::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;
}

当shared_ptr处理双向链表的链接过程中出现了循环引用的现象,造成了内存泄露:

在这里插入图片描述

为何会出现循环引用的现象,下面我们来分析一下:

  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成员,所以这就叫循环引用,谁也不会释放。

在这里插入图片描述

如果我只链接一方的话,就不会出现内存泄露了,我们来分析一下:

在这里插入图片描述
在这里插入图片描述

那么该如何解决这个问题呢?本质上出现这个问题的原因就是因为双向链表节点的成员_next与_prev成员参与了对资源的管理——即引用计数,那么我们想办法让它们不再参与对资源的管理就可以解决问题了。C++库中设计了weak_ptr类来专门解决shared_ptr出现循环引用的问题。

2.5.3 weak_ptr

weak_ptr性质:

  • 它不是常规的智能指针,不支持RAII
  • 支持像指针一样使用,可以指向资源,但是他不参与管理,不增加引用计数
  • 专门设计出来辅助解决shared_ptr的循环引用问题

weak_ptr的模拟设计:

namespace curry
{
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		
		// 利用shared_ptr进行构造 - 只初始化_ptr, 并没有支持引用计数的管理
		weak_ptr(shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

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

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

		T* get()
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};
}

我们只需把节点里的成员_next_prev设计为weak_ptr管理即可解决问题:

在这里插入图片描述

2.6 删除器

如果不是new出来的对象如何通过智能指针管理呢?或者是 new[ ] 出来的对象如果通过智能指针管理呢?那么就需要给 shared_ptr 设计定制删除器来解决这个问题。

namespace curry
{
	template<class T>
	struct DeleteArray
	{
		void operator()(T* ptr)
		{
			cout << "delete[] ptr: " << ptr << endl;
			delete[] ptr;
		}
	};

	template<class T>
	struct FreeFunc
	{
		void operator()(T* ptr)
		{
			cout << "free:" << ptr << endl;
			free(ptr);
		}
	};

	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _pmtx(new mutex)
		{}
		
		// 在类模板里面构造一个删除包装器
		template<class D>
		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _pmtx(new mutex)
			, _del(del)
		{}

		shared_ptr(const shared_ptr<T>& ap)
			: _ptr(ap._ptr)
			, _pcount(ap._pcount)
			, _pmtx(ap._pmtx)
		{
			AddRef();
		}

		void Release()
		{
			_pmtx->lock();
			bool flag = false;
			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					_del(_ptr);  // 使用包装器去调用对应的operator()删除函数
				}
			}
			delete _pcount;
			flag = true;

			// 在释放锁之前要释放掉相应的资源
			_pmtx->unlock();
			if (flag == true)
			{
				delete _pmtx;
			}
		}

		void AddRef()
		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}

		shared_ptr<T>& operator=(const shared_ptr& ap)
		{
			if (_ptr != ap._ptr)
			{
				Release();

				_ptr = ap._ptr;
				_pcount = ap._pcount;
				_pmtx = ap._pmtx;

				AddRef();
			}

			return *this;
		}

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

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

		T* get()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}

		~shared_ptr()
		{
			Release();
		}
	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;

		// 包装器
		function<void(T*)> _del = [](T* ptr) {
			cout << "delete:" << ptr << endl;
			delete ptr;
		};
	};

	struct Date
	{
		int _year = 0;
		int _month = 0;
		int _day = 0;

		~Date()
		{}
	};

	void test_shared_deletor()
	{
		curry::shared_ptr<Date> sp0(new Date);
		curry::shared_ptr<int> sp1((int*)malloc(4), FreeFunc<int>());

		curry::shared_ptr<Date> spa2(new Date[10], DeleteArray<Date>());
		curry::shared_ptr<Date> spa3(new Date[10], [](Date* ptr) {
			cout << "lambda delete[]:" << ptr << endl;
			delete[] ptr;
			});

		curry::shared_ptr<FILE> spa4(fopen("test.cpp", "r"), [](FILE* ptr) {
			cout << "lambda fclose:" << ptr << endl;
			fclose(ptr);
			});
	}
}

在这里插入图片描述