C++ 智能指针

💓博主CSDN主页:麻辣韭菜💓

⏩专栏分类:C++修炼之路

🚚代码仓库:C++高阶🚚

🌹关注我🫵带你学习更多C++知识
  🔝🔝

目录

引言

 1. 为什么需要智能指针?

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

2.1 RAII

2.1.1 C++98 auto_ptr 

2.1.2 unique_ptr

2.1.3 shared_ptr

shared_ptr的循环引用  

2.1.4 weak_ptr

 3.定制删除器


引言

通过前面的异常学习,我们知道捕捉到异常会直接导致代码跳转执行到catch进行处理,如果这段异常代码涉及到内存管理,那么就会造成内存泄漏,整个工程最后申请不到内存资源。为了解决异常跳转执行而引发的其他问题,C++98最早推出了auto_ptr。但是这个指针在设计出来时就留下了很的多坑,所以在C++11后推出全新的智能指针。

 1. 为什么需要智能指针?

我们模拟一个异常的场景

#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()
{
	int* ptr1 = new int;
	int* ptr2 = new int;
	cout << Div() << endl;
	delete ptr1;
	delete ptr2;
}

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

这里Func如果出现除0错误,那么就会导致后面的delete无法执行,从而导致内存泄漏。这时有人就会想到在出现异常地方从新 try throw catch进行重新抛出。

就比如下面这段代码

#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()
{
	int* ptr1 = new int;
	int* ptr2 = nullptr;
	try //ptr2 出现异常
	{
		ptr2 = new int;
	}
	catch(...)
	{
		delete ptr1;
		throw;
	}
	try //Div出现除0异常
	{
		cout << Div() << endl;
	}
	catch (...)
	{
		delete ptr1;
		delete ptr2;
		throw;
	}
	delete ptr1;
	delete ptr2;
}

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

这段代码确实可以解决内存泄漏的问题,但是如果再来一个ptr3一直到ptrn?,那我们都像上面的try throw catch 这样?这代码看着也烦,而且一点也不优雅。于是大佬们利用ARII的思想来解决这个问题。

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

2.1 RAII

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

这里和互斥锁那里是一样的。

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	
	~SmartPtr()
	{
		if (_ptr)
		{
			cout << "~SmartPtr():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()
{
	SmartPtr<int> p1(new int(1));
	SmartPtr<int> p2(new int(2));

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

  

通过我们编写的smartPtr这个类,利用成员的函数特性,自动调用析构函数。也确实在除0异常出现要跳转时,先调用了析构函数。但是话说智能指针还是指针,我们的类也需要想指针一样能使用。

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
		T& operator*() 

	{
		return *_ptr;
	}

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

 这样对象也能解引用了,对于自定义类型的我们也可以用->。这些都比较简单。问题的关键是如何写拷贝和赋值重载?

比如下面一段代码?

SmartPtr<int> p3(p2);

p3拷贝p2 我们看看运行结果

  

代码就直接崩溃了,原因很简单,p3和p2同时指向了同一块空间,p2先析构,等p3再析构时,野指针了。

既然指向同一块空间,深拷贝?深拷贝不行,问题是指针本身就是要浅拷贝。STL的容器都是浅拷贝。迭代器为什么不报错?迭代器本身自己就不涉及资源的管理,而智能指针涉及资源的管理,所以不能单纯的浅拷贝

到这里就要说说一下智能指针的发展历史

2.1.1 C++98 auto_ptr 

既然指向同一块空间,那被拷贝对象的资源直接转移给拷贝对象。C++98版本的库中就提供了auto_ptr的智能指针。

下面演示的auto_ptr的使用及问题。

	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete" << _ptr << endl;
				delete _ptr;
			}
		}
	private:
		T* _ptr;
	};

这样确实没有问题。如果有人不知道auto_ptr 会把 p1置空?就像下面这段代码

 直接就崩了,所以说auto_ptr的管理权转移是个失败品。

由于auto_ptr的失败C++11推出了unique_ptr

2.1.2 unique_ptr

这个思路也是简单,既然拷贝要出事,那干脆直接就禁掉拷贝和赋值。

template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
		unique_ptr(unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

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

这样做确实也能防止拷贝的问题,有些场景就是需要拷贝怎么办?后面C++11又推出了shared_ptr。利用引用计数的思想来解决。

2.1.3 shared_ptr

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

2. 对象被销毁时 ( 也就是析构函数调用 ) ,就说明自己不使用该资源了,对象的引用计数减
一。
3. 如果引用计数是 0 ,就说明自己是最后一个使用该资源的对象, 必须释放该资源
4. 如果不是 0 ,就说明除了自己还有其他对象在使用该份资源, 不能释放该资源 ,否则其他对
象就成野指针了。

template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_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) //防止自己给自己赋值
			{
				Release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);

			}
			return *this;
		}
		

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

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

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

		}

		~shared_ptr()
		{
			Release();
		}
	private:
		T* _ptr;
		int* _pcount; //如果是static成员变量,那么属于所有对象。
	};
	void test_shared()
	{
		shared_ptr<int> sp1(new int(1));
		shared_ptr<int> sp2(sp1);
		shared_ptr<int> sp3(sp2);

		shared_ptr<int> sp4(new int(10));

		//sp1 = sp4;
		sp4 = sp1;

		sp1 = sp1;
		sp1 = sp2;
	}

 

到这里就完了吗? 如果是多个线程执行这个shared肯定会有线程安全的问题,_pcount是new出来的,是堆资源、++ --又不是原子操作。 所以对shared的成员_pcount需要加锁,当然我们也可以对成员直接变成原子的。

加锁版本

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

		~shared_ptr()
		{
			Release();
		}

		void Release()
		{
			
			int flag = false;
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
				unique_lock<mutex> lck(*_pmtx);//在这里对--加锁
				flag = true;
			}
			if (flag)
				delete _pmtx;
		}

		void AddCount()
		{
			unique_lock<mutex> lck(*_pmtx);
			++(*_pcount);
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pmtx(sp._pmtx)
		{
			AddCount();
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmtx = sp._pmtx;

				AddCount();
			}

			return *this;
		}

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

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

		T* get()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}
	private:
		T* _ptr;          // 指向管理对象的指针
		int* _pcount;     // 引用计数
		mutex* _pmtx; // 互斥锁,用于同步对引用计数的访问
	};

原子版本

template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			: _ptr(ptr)
			, _pcount(new std::atomic<int>(1)) 
		{}
		// 复制构造函数
		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pcount(sp._pcount) 
		{
			SubAdd();
		}
		// 赋值操作符
		shared_ptr<T>& operator=(const shared_ptr<T>& sp) 
		{
			if (this != &sp) 
			{
				// 先递减当前对象的引用计数
				if (_pcount->fetch_sub(1, std::memory_order_acq_rel) == 1)
				{
					delete _ptr;
					delete _pcount;
				}
				// 然后复制新对象的指针和引用计数
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				// 递增新对象的引用计数
				SubAdd();
			}
			return *this;
		}
		void SubAdd()
		{	// 自动递增引用计数
			_pcount->fetch_add(1, std::memory_order_relaxed);
		}
		// 解引用操作符
		T& operator*() { return *_ptr; }
		// 成员访问操作符
		T* operator->() { return _ptr; }

		// 析构函数
		~shared_ptr() 
		{
			if (_pcount->fetch_sub(1, std::memory_order_acq_rel) == 1)
			{
				delete _ptr;
				delete _pcount;
			}
		}

		T* get()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}
	private:
		T* _ptr;
		std::atomic<int>* _pcount;
	};

 

结果是1 原子版本也没有问题。

这里需要说明的是为什么用ref()这个函数,原因很简单,

智能指针的参数和锁的参数都是引用,但是我们是以线程调用的,而线程构造其函数的参数,是禁止拷贝的。 因为引用本身不是一个对象,而是一个指向对象的别名。如果尝试直接传递引用,编译器无法为其创建一个副本,因为引用不具有复制或移动语义。

使用 std::ref() 的目的在于告诉 std::thread 构造函数:“我知道我要传递的是一个引用,并且我希望你以引用的方式来处理它。”

智能指针是安全的,智能指针管理的对象是安全的吗?

 

结果来看我们也是需要对对象涉及的临界资源进行加锁 

 

shared_ptr的循环引用  

我先来一段简单代码看看运行结果

	struct ListNode
	{
		ListNode* _next;
		ListNode* _prev;
		int _val;
		~ListNode()
		{
			cout << "~ListNode()" << endl;
		}
	};
	//循环引用
	void test_shared_cycle()
	{
		ListNode* n1 = new ListNode;
		ListNode* n2 = new ListNode;

		n1->_next = n2;
		n2->_prev = n1;

		delete n1;
		delete n2;

	}

 

我们调用test_shared_cycle() 函数能够正常析构。如果我把上面的代码改成用shared_ptr来管理list类会发生什么?

struct ListNode
	{
		gx::shared_ptr<ListNode> _next;
		gx::shared_ptr<ListNode> _prev;
		int _val;
		~ListNode()
		{
			cout << "~ListNode()" << endl;
		}
	};
	//循环引用
	void test_shared_cycle()
	{
		/*ListNode* n1 = new ListNode;
		ListNode* n2 = new ListNode;*/
		gx::shared_ptr<ListNode> n1 = new ListNode;
		gx::shared_ptr<ListNode> n2 = new ListNode;

		n1->_next = n2;
		n2->_prev = n1;

	}

运行的结果并没有运行ListNode的析构函数。

如果我们把n1和n2链接任何一个取消都会得到正常的释放

那么上面的问题是如何产生的?

n1n2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上 一个节点。
也就是说_next析构了,n2就释放了。
也就是说_prev析构了,n1就释放了。
但是_next属于n1的成员,n1释放了,_next才会析构,而n1_prev管理,_prev
属于n2成员,所以这就叫循环引用,谁也不会释放。

为了解决这个问题,C++11又推出了weak_ptr。 

2.1.4 weak_ptr

如何解决shared_ptr的循环引用?只要n1和n2在链接的过程中,不增加引用计数就行。

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

	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get())
	{}

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

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

	T* get()
	{
		return _ptr;
	}

private:
	T* _ptr;
};

 

运行结果来看weak_ptr解决这里引用循环的问题。 我们打印引用计数看看。

这里需要强调的是,我们实现的都是智能指针最核心的部分,库里的源代码不是我们这样实现的,库里要考虑的场景更多,比如内存碎片,库源代码要复杂的更多。

 3.定制删除器

struct Date
	{
		int _year = 0;
		int _month = 0;
		int _day = 0;
		~Date() {};
	};
void test_shared_delete()
	{
		gx::shared_ptr<Date> spd(new Date[10]);
	}

 

这时我们用shared_ptr就会出现报错的原因。

核心原因:是因为我们写了析构函数,在实例化对象时,会多开4个字节,而shared_ptr的析构函数释放位置就会从多开的4个字节这里开始释放。释放的位置不对,程序崩溃是必然。内存错误。 

针对上面的问题我们需要用到定制删除器

 从官方文档的构造函数来看,定制删除器,是模板只要是能调用的对象都能传参。比如lambda、仿函数、函数、函数指针。

先自己写一个仿函数,然后我们用库的shared_ptr。

	template<class T>
	struct DeleteArry
	{
		void operator(T* ptr)
		{
			cout << "void operator(T* ptr)" << endl;
			delete[] ptr;
		}
	};
void test_shared_delete()
	{	//仿函数
		std::shared_ptr<Date> spd0(new Date[10],DeleteArry<Date>());
		//lambda
		std::shared_ptr<Date> spd1(new Date[10], [](Date* ptr) 
			{	cout << "Lambda delete[]" << endl;
				delete[] ptr;
			}
		);
		//文件指针
		std::shared_ptr<FILE> spd2(fopen("Test.cpp","r"), [](FILE* ptr)
			{	cout << "Lambda fclose:" << endl;
				fclose(ptr);
			}
		);
	}

 

那我们如何在自己的shared_ptr实现这一功能?

库里的定制删除器是个模板,传过去时,库里是存起来的。所以我们也需要写一个存储定制删除器的构造函数。

template<class D>
		shared_ptr(T* ptr, D del)
			:_ptr(ptr)
			, _pcount(new int(1))
			, _pmtx(new mutex)
			, _del(del)
		{}

         

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

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

	void AddCount()
	{
		_pmtx->lock();

		++(*_pcount);

		_pmtx->unlock();
	}

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

	// sp1 = sp4
	// sp1 = sp1;
	// sp1 = sp2;
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			Release();

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp._pmtx;

			AddCount();
		}

		return *this;
	}

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

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

	T* get() const
	{
		return _ptr;
	}
	T* get() 
	{
		return _ptr;
	}

	int use_count()
	{
		return *_pcount;
	}

private:
	T* _ptr;
	int* _pcount;
	mutex* _pmtx;
	// D _del; //如果是这样就不行,因为这个D是属于定制删除器构造成员函数的,析构是用不了的
	 //包装器
	function<void(T*)> _del = [](T* ptr) {
		cout << "lambda delete:" << ptr << endl;
		delete ptr;
	};
};

 

 总结:

智能指针根据自己的需要到底是使用unique、shared、weak。

不考虑拷贝,unique

涉及拷贝  shared

如果是list map set unorderdmap unorderdset 这种需要用到weak

  • 24
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值