C++ 智能指针

文章目录

前言

本文将会向你介绍C++智能指针,重在介绍智能指针的发展历程以及shared_ptr的模拟实现

为什么要有智能指针

先观察以下代码,并思考
如果p1的new抛异常、p2的new抛异常、div除法函数如果除0会怎样?

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

void Func()
{
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	cout << 1 << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

之前可能只是new出来,忘记释放了,导致内存泄露,而异常出来后,情况就复杂了,异常会改变执行流,直接往catch处跳了,不会执行delete语句,会出现内存泄漏的问题,像new出错抛异常有时又是我们规避不了的,那该怎么办呢?

->采用RAII思想管理资源(把管理一份的资源的责任托管给了一个对象,不需要手动地去delete)

智能指针的使用及原理

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

使用RAII思想可以设计出SmartPtr类,这也是智能指针的雏形

cpp 复制代码
	template<class T>
	class SmartPtr
	{
	public:
		SmartPtr(T* ptr)
			:_ptr(ptr)
		{}

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

当一个指针赋值给另一个指针时,我们需要的是浅拷贝,就是想让两个指针

指向同一块空间,但是指向了同一块空间就会出现析构两次的问题

auto_ptr

C++98就已经在库中实现了auto_ptr, auto_ptr的理念是既然有析构两次的风险,那么把A指针赋值给B指针后,A指针就销毁不能用了,即管理权转移,会把被拷贝对象的资源管理权转移给拷贝对象,这时我们再访问A指针,对于一些不懂auto_ptr的人,可是大灾难

cpp 复制代码
	template<class T>
	class auto_ptr
	{
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~auto_ptr()
		{
			cout << "delete: " << _ptr << endl;
			delete _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T& operator->()
		{
			return _ptr;
		}
		//auto_ptr解决两次析构的理念
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
	private:
		T* _ptr;
	};

unique_ptr

后来,为了填auto_ptr的坑,C++11推出了新的智能指针:unique_ptr Unique_ptr的理念就比较绝了,既然是拷贝、赋值导致的析构两次的问题 Unique_ptr直接就把拷贝和赋值给禁了 明显地还没到不死不休的情况,把拷贝和赋值给禁了就太绝了,因此auto_ptr 在很多公司中明令禁止使用

cpp 复制代码
template<class T>
	class unique_ptr
	{
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			cout << "delete: " << _ptr << endl;
			delete _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T& operator->()
		{
			return _ptr;
		}
		unique_ptr(unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;
	private:
		T* _ptr;
	};

shared_ptr

再后来,C++11也就有了Shared_ptr,shared_ptr的理念是使用引用计数的思想,对析构作修改,如果有两个智能指针同时对一个资源的管理,此时引用计数为2,实际上每次析构的时候只会让引用计数--,实际只会释放一次,这样就很好地解决了赋值、拷贝析构两次的问题

shared_ptr需要重点掌握,因为面试中hr可能要求手撕

shared_ptr的重点在拷贝与赋值上,在前一代智能指针的基础上,增加了一个引用计数,引用计数资源也是由一个指针进行管理

赋值重载中我们需要注意,当 sp1=sp2,我们是需要将sp1管理资源的引用计数进行- -的,同时对sp2、sp4、sp5共同管理资源的引用计数++

还需要判断是否是自己给自己赋值,即sp1 = sp1,sp2 = sp5(本质上也是自己给自己赋值)

cpp 复制代码
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _count(new int(1))
		{}
		~shared_ptr()
		{
			if (--(*_count) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _count;
			}
		}
		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _count(sp._count)
		{
			++(*_count);
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//判断是否是给自己赋值
			if (_ptr == sp._ptr)
			{
				return *this;	//最好是返回左操作数
			}
			//减少赋值对象的引用计数
			if (--(*_count) == 0)
			{
				delete _ptr;
				delete _count;
			}
			_ptr = sp._ptr;
			_count = sp._count;
			//加加此时的引用计数
			++(*_count);
			return *this;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T& operator->()
		{
			return _ptr;
		}
		int use_count() const
		{
			return *_count;
		}
		T* get() const
		{
			return *_ptr;
		}
	private:
		T* _ptr;
		int* _count;	//引用计数
	};

循环引用

尽管shared_ptr已经很完美了,但仍然是有缺陷的

cpp 复制代码
struct ListNode
{
	int _data;
	shared_ptr<ListNode> prev;
	shared_ptr<ListNode> next;
	~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	node1->next = node2;
	//node2->prev = node1;
	return 0;
}

如果屏蔽掉node2->prev = node1;node1->next = node2; 是不会构成循环引用的,此时node1,node2都可以正常释放

如果构成循环引用,node1与node2都得不到释放,从而内存泄漏
以下就是循环引用问题。这样双方都不会进行析构

当两份空间还没连接起来,只有node1与node2指向空间,引用计数都为1,当连接后,它们的引用计数都变为2了,当main函数调用完,node2先析构,此时引用计数变为1,node1再析构,引用计数也减为1,但是这两份空间不会释放,因为引用计数还没有减到0,那么什么时候才会释放,要node2的prev析构后,node1空间才会析构...如图所示

解决方法一:在使用智能指针的时候直接跳过循环引用这个坑

解决方法二:换成弱指针weak_ptr

weak_ptr

weak ptr不是RAII思想的智能指针,只是专门用来解决share_ptr的循环引用问题的,weak_ptr的拷贝与赋值是支持shared_ptr进行拷贝与赋值的

cpp 复制代码
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T& operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

这样就解决了循环引用问题,node1与node2节点都进行析构了

小结

今日的分享就到这里了,重点掌握智能指针的发展历程中每个智能指针的缺陷是啥,以及需要掌握shared_ptr的手撕

相关推荐
MengYiKeNan1 小时前
C++二分函数lower_bound和upper_bound的用法
开发语言·c++·算法
小林熬夜学编程2 小时前
C++第五十一弹---IO流实战:高效文件读写与格式化输出
c语言·开发语言·c++·算法
月夕花晨3742 小时前
C++学习笔记(30)
c++·笔记·学习
蠢蠢的打码2 小时前
8584 循环队列的基本操作
数据结构·c++·算法·链表·图论
不是编程家2 小时前
C++ 第三讲:内存管理
java·开发语言·c++
jianglq2 小时前
C++高性能线性代数库Armadillo入门
c++·线性代数
Lenyiin4 小时前
《 C++ 修炼全景指南:十 》自平衡的艺术:深入了解 AVL 树的核心原理与实现
数据结构·c++·stl
程序猿练习生4 小时前
C++速通LeetCode中等第5题-无重复字符的最长字串
开发语言·c++·leetcode
无名之逆4 小时前
云原生(Cloud Native)
开发语言·c++·算法·云原生·面试·职场和发展·大学期末
好蛊4 小时前
第 2 课 春晓——cout 语句
c++·算法