【C++】智能指针

👀樊梓慕:个人主页

 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》

🌝每一个不曾起舞的日子,都是对生命的辜负


目录

前言

为什么引入智能指针?

什么是内存泄漏?

内存泄漏的分类

如何检测内存泄漏 

如何避免内存泄漏

场景搭建 

什么是智能指针?

智能指针的拷贝问题

C++标准库中的智能指针

auto_ptr模拟实现

unique_ptr模拟实现

shared_ptr模拟实现

引用计数的设计思路剖析

普通成员变量行不行?

静态成员变量行不行?

最终解决方案

定制删除器 

什么是定制删除器?

shared_ptr带『 定制删除器』模拟实现 

shared_ptr的线程安全问题 

weak_ptr模拟实现

循环引用问题剖析

循环引用问题是什么?

通过weak_ptr解决循环引用问题


前言

在『 异常』部分我们提到了内存泄漏的问题,当时我们解决的方案是『 异常的重新抛出』,但很明显这种方案虽然有效,但是这无疑让我们的代码看起来并不整齐且复杂,那么C++为了预防内存泄漏,给我们提供了一种解决方案:智能指针,接下来就让我们一起来学习吧。


欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。 

=========================================================================

GITEE相关代码:🌟樊飞 (fanfei_c) - Gitee.com🌟

=========================================================================


为什么引入智能指针?

什么是内存泄漏?

内存泄露是指因为疏忽或错误,造成程序未能释放已经不再使用的内存的情况。

内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

内存泄漏的分类

C/C++程序中一般我们关心两种方面的内存泄漏:

  • 堆内存泄漏(Heap leak)

堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

  • 系统资源泄漏

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定等问题。

如何检测内存泄漏 

在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/gatieme/article/details/51959654在windows下使用第三方工具:

VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库_visual leak detector vs2020-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/GZrhaunt/article/details/56839765其他工具:

内存泄露检测工具比较 - 默默淡然 - 博客园 (cnblogs.com)icon-default.png?t=N7T8https://www.cnblogs.com/liangxiaofeng/p/4318499.html

如何避免内存泄漏

  • 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。但是如果碰上异常时,就算注意释放了,还是可能会出问题。
  • 采用RAII思想或者智能指针来管理资源。
  • 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  • 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵

场景搭建 

比如:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* ptr = new int;
	//...
	cout << div() << endl;
	//...
	delete ptr;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

我们知道当try块中抛出异常后,会直接进行捕获执行catch块代码,那么这里就会产生一个问题:new出来的对象还没有delete释放,这样的场景就是典型的『 内存泄漏』。

在上节讲解异常时,我们提出了『 异常的重新抛出』解决方案。

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* ptr = new int;
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete ptr;
		throw;
	}
	delete ptr;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

异常的重新抛出解决问题的原理是:在抛出异常后,在func函数中先对div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出。

这样就确保了当有异常抛出时,我们new出来的空间也可以得到及时的释放,进而放置『 内存泄漏』。

但是如果是下面这种情况呢?

void fxx()
{
	int* p1 = new int[10];
	int* p2, *p3;
	try
	{
		p2 = new int[20];
		try {
			p3 = new int[30];
		}
		catch (...)
		{
			delete[] p1;
			delete[] p2;
			throw;
		}
	}
	catch (...)
	{
		delete[] p1;
		throw;
	}

	//...

	delete[] p1;
	delete[] p2;
	delete[] p3;
}

如果这里我们依旧利用『 异常的重新抛出』来解决,肉眼可见的:代码变得复杂且丑陋。

所以C++引入了『 智能指针』。


什么是智能指针?

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

以我的理解就是:将申请到的内存空间交给一个智能指针对象进行管理,这个指针的用法与普通指针没有差别(通过重载解引用*操作符,箭头->操作符)。

那么为什么这么做,或者说这样做有什么好处呢?

我们还是举例来分析:

//智能指针类
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;
};

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

void func()
{
    //将申请到的空间交给智能指针sp管理
	SmartPtr<int> sp(new int);
	//...
	cout << div() << endl;
	//...
}

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

代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。

  • 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来。
  • 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放。
  • 此外,为了让SmartPtr对象能够像原生指针一样使用,需要对*和->运算符进行重载。

这样做的目的就是在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束(比如由于catch跳出了当前函数栈)就会调用其对应的析构函数,进而完成内存资源的释放。


智能指针的拷贝问题

对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃。比如:

int main()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1); //拷贝构造

	SmartPtr<int> sp3(new int);
	SmartPtr<int> sp4(new int);
	sp3 = sp4; //拷贝赋值

	return 0;
}

主要原因是这里的拷贝构造与拷贝赋值都是编译器自动生成的默认成员函数,默认为浅拷贝,而浅拷贝相当于sp1和sp2管理了同一块内存空间,这样在析构时就会发生『 二次析构』的问题。

但这里我们恰恰需要的就是浅拷贝。

  • 智能指针与迭代器需要的就是浅拷贝:因为本质资源不是自己的,只是代为持有,他们拷贝的时候期望指向同一个资源。
  • 而容器比如vector需要的是深拷贝:因为他们要利用资源存储管理数据,资源是自己的。

当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此为了解决智能指针的拷贝问题,衍生出了不同版本的智能指针。


C++标准库中的智能指针

  • auto_ptr:失败之作。

简单地通过『 管理权转移』的方式解决拷贝问题,也就是说当发生拷贝或赋值时,被拷贝对象把资源管理权转移给拷贝对象,然后置空被拷贝对象,保证一个资源在任何时刻都只有一个对象在对其进行管理,但这很危险,很多公司都禁止使用auto_ptr。

  • unique_ptr:简单粗暴地禁止拷贝。

顾名思义,唯一的指针,还记得C++11中的delete关键字么,也就是说unique_ptr不允许生成默认的拷贝构造和赋值运算符重载。

  • shared_ptr:比较靠谱,通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

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

  • weak_ptr:不是用来管理资源的释放的,为了解决shared_ptr的循环引用问题而生。

具体请看代码实现部分。

auto_ptr模拟实现

// 原理:RAII + 具有指针类似行为 + 解决浅拷贝问题
// RAII: 对用户资源进行管理,构造中接受,析构函数中释放,用户可以不管用关心资源何时释放,对象销毁时,编译器会调用析构函数释放资源
// 具有指针类似行为:重载operator*()和operator->()
// 解决浅拷贝:采用资源管理权转移处理,在拷贝构造和赋值运算符重载中,将参数对象中资源转移给当前对象,参数对象与资源断开联系,以此来保证每个资源只释放一次
namespace F
{
	template<class T>
	class auto_ptr
	{
	public:
		//RAII
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~auto_ptr()
		{
			if (_ptr != nullptr)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr; //管理权转移后ap被置空
		}
		auto_ptr& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				delete _ptr;       //释放自己管理的资源
				_ptr = ap._ptr;    //接管ap对象的资源
				ap._ptr = nullptr; //管理权转移后ap被置空
			}
			return *this;
		}
		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr; //管理的资源
	};
}

unique_ptr模拟实现

// 智能指针原理:ARII + 具有指针类似行为 + 解决浅拷贝
// unique_ptr: 资源独占的方式(一份资源只能被一个对象管理)
// 方式:禁止拷贝构造和赋值运算符重载调用---防拷贝
namespace F
{
	template<class T>
	class unique_ptr
	{
	public:
		//RAII
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr != nullptr)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}
		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//防拷贝
		unique_ptr(unique_ptr<T>& up) = delete;
		unique_ptr& operator=(unique_ptr<T>& up) = delete;
	private:
		T* _ptr; //管理的资源
	};
}

shared_ptr模拟实现

/*
shared_ptr实现原理: RAII + 指针类似的行为 + 引用计数方式共享资源
*/
namespace F
{
	template<class T>
	class shared_ptr
	{
	public:
		//RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				if (_ptr != nullptr)
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					_ptr = nullptr;
				}
				delete _pcount;
				_pcount = nullptr;
			}
		}
		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			(*_pcount)++;
		}
		shared_ptr& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr) //管理同一块空间的对象之间无需进行赋值操作
			{
				if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;       //与sp对象一同管理它的资源
				_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
				(*_pcount)++;         //新增一个对象来管理该资源,引用计数++
			}
			return *this;
		}
		//获取引用计数
		int use_count()
		{
			return *_pcount;
		}
		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;      //管理的资源
		int* _pcount; //管理的资源对应的引用计数
	};
}

引用计数的设计思路剖析

为什么这里设计的引用计数在堆区呢?

普通成员变量行不行?

首先shared_ptr的引用计数不能单纯的定义成一个int类型的成员变量,因为这就意味着每个shared_ptr对象都有一个自己的count成员变量,而实际的场景是当多个对象要管理同一个资源时,这几个对象应该用到的是同一个引用计数。

静态成员变量行不行?

其次,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量一个类中只有一份,shared_ptr类管理好几份资源时,很明显就不足以满足需求了。

最终解决方案

而如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时(构造)就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。

这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。


定制删除器 

什么是定制删除器?

根据以上对shared_ptr的模拟实现,你会发现好像智能指针都只能管理一个对象,那么如果智能指针要管理一些对象,或者是文件对象呢?

比如:

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10]);   //error
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r")); //error

	return 0;
}

如果管理的是这样的对象,我们提供的析构函数中的delete就不能满足需求了。

因为以『 new[]』的方式申请到的内存空间必须以『 delete[]』的方式进行释放,而文件指针必须通过调用『 fclose』进行释放。 

所以,这时就需要用到定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数:

template <class U, class D>
shared_ptr (U* p, D del);

参数说明:

  • p:需要让智能指针管理的资源。
  • del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。

因此当智能指针管理的资源不是以new的方式申请到的内存空间时,就需要在构造智能指针对象时传入定制的删除器。比如:

template<class T>
struct DelArr
{
	void operator()(const T* ptr)
	{
		cout << "delete[]: " << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>());
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
	});

	return 0;
}

那么现在我们学会了使用,现在让我们一起来探究下定制删除器的底层是如何实现的吧。


shared_ptr带『 定制删除器』模拟实现 
template<class T>
class shared_ptr
{
public:
	// function<void(T*)> _del = [](T* ptr) {delete ptr; };//放到私有里面
	template<class D>
	shared_ptr(T* ptr, D del)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _del(del)
	{}

	// RAII
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}

	// sp2(sp1)
	shared_ptr(const shared_ptr<T>& sp)
	{
		_ptr = sp._ptr;
		_pcount = sp._pcount;

		// 拷贝时++计数
		++(*_pcount);
	}

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

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

			// 拷贝时++计数
			++(*_pcount);
		}

		return *this;
	}

	void release()
	{
		// 说明最后一个管理对象析构了,可以释放资源了
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			//delete _ptr;
			_del(_ptr); //调用定制删除器

			delete _pcount;
		}
	}

	~shared_ptr()
	{
		// 析构时,--计数,计数减到0,
		release();
	}

	int use_count()
	{
		return *_pcount;
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	T* get() const
	{
		return _ptr;
	}
private:
	T* _ptr;     // 指向动态分配对象的指针  
	int* _pcount;// 引用计数,表示当前有多少个shared_ptr指向同一个对象  

	function<void(T*)> _del = [](T* ptr) {delete ptr; };// 自定义删除器,默认为删除指针所指向的对象
};


shared_ptr的线程安全问题 

当前模拟实现的shared_ptr还存在线程安全的问题,由于管理同一个资源的多个对象的引用计数是共享的,因此多个线程可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否则就会导致线程安全问题。

要解决引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数进行封装,这里以加锁为例。

  • 在shared_ptr类中新增互斥锁成员变量,为了让管理同一个资源的多个线程访问到的是同一个互斥锁,管理不同资源的线程访问到的是不同的互斥锁,因此互斥锁也需要在堆区创建。
  • 在调用拷贝构造函数和拷贝赋值函数时,除了需要将对应的资源和引用计数交给当前对象管理之外,还需要将对应的互斥锁也交给当前对象。
  • 当一个资源对应的引用计数减为0时,除了需要将对应的资源和引用计数进行释放,由于互斥锁也是在堆区创建的,因此还需要将对应的互斥锁进行释放。
  • 为了简化代码逻辑,可以将拷贝构造函数和拷贝赋值函数中引用计数的自增操作提取出来,封装成AddRef函数,将拷贝赋值函数和析构函数中引用计数的自减操作提取出来,封装成ReleaseRef函数,这样就只需要对AddRef和ReleaseRef函数进行加锁保护即可。
template<class T>
class shared_ptr
{
private:
    //++引用计数
	void AddRef()
	{
		_pmutex->lock();
		(*_pcount)++;
		_pmutex->unlock();
	}
	//--引用计数
	void ReleaseRef()
	{
		_pmutex->lock();
		bool flag = false;
		if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
		{
			if (_ptr != nullptr)
			{
				cout << "delete: " << _ptr << endl;
				_del(_ptr);
				_ptr = nullptr;
			}
			delete _pcount;
			_pcount = nullptr;
			flag = true;
		}
		_pmutex->unlock();
		if (flag == true)
		{
			delete _pmutex;
		}
	}
public:
	// function<void(T*)> _del = [](T* ptr) {delete ptr; };//放到私有里面
	template<class D>
	shared_ptr(T* ptr, D del)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _del(del)
        , _pmutex(new mutex)
	{}

	// RAII
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
        , _pmutex(new mutex)
	{}

	// sp2(sp1)
	shared_ptr(const shared_ptr<T>& sp) 
        : _ptr(sp._ptr) 
        , _pcount(sp._pcount)
        , _pmutex(sp._pmutex)
	{
		// 拷贝时++计数
		AddRef();
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//if (this != &sp)
		if (_ptr != sp._ptr) //管理同一块空间的对象之间无需进行赋值操作
		{
			ReleaseRef();        //将管理的资源对应的引用计数--

			_ptr = sp._ptr;      //与sp对象一同管理它的资源
			_pcount = sp._pcount;//获取sp对象管理的资源对应的引用计数
            _pmutex = sp._pmutex;//获取sp对象管理的资源对应的互斥锁

			// 拷贝时++计数
			AddRef();            //新增一个对象来管理该资源,引用计数++
		}

		return *this;
	}

	~shared_ptr()
	{
		// 析构时,--计数,计数减到0,
		ReleaseRef();
	}

	int use_count()
	{
		return *_pcount;
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	T* get() const
	{
		return _ptr;
	}
private:
	T* _ptr;     // 指向动态分配对象的指针  
	int* _pcount;// 引用计数,表示当前有多少个shared_ptr指向同一个对象  
    mutex* _pmutex;// 管理的资源对应的互斥锁

	function<void(T*)> _del = [](T* ptr) {delete ptr; };// 自定义删除器,默认为删除指针所指向的对象
};

weak_ptr模拟实现

简易版的weak_ptr的实现步骤如下:

  1. 提供一个无参的构造函数,比如刚才new ListNode时就会调用weak_ptr的无参的构造函数。
  2. 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr对象管理的资源。
  3. 支持用shared_ptr对象拷贝赋值给weak_ptr对象,赋值时获取shared_ptr对象管理的资源。
  4. 对*和->运算符进行重载,使weak_ptr对象具有指针一样的行为。
namespace F
{
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}
		weak_ptr& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}
		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr; //管理的资源
	};
}

循环引用问题剖析

循环引用问题是什么?

循环引用问题是在使用shared_ptr时出现的一种问题,在一些特定的场景下才会产生。

搭建场景

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	ListNode* node1 = new ListNode;
	ListNode* node2 = new ListNode;

	node1->_next = node2;
	node2->_prev = node1;
	//...
	delete node1;
	delete node2;
	return 0;
}

现在我们学习了异常,学习了智能指针,所以我们很容易发现这段代码容器引发『 内存泄漏』。

所以我们利用shared_ptr对代码进行优化:

struct ListNode
{
    // 注意:别忘了需要把ListNode类中的next和prev成员变量的类型也改为shared_ptr类型。
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	node1->_next = node2;
	node2->_prev = node1;
	//...

	return 0;
}

运行程序发现:两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用。

1.当以new的方式申请到两个ListNode结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1。

2.将这两个结点连接起来后,资源1当中的_next成员与node2一同管理资源2,资源2中的_prev成员与node1一同管理资源1,此时这两个资源对应的引用计数都被加到了2。

 3.当出了main函数的作用域后,node1和node2的生命周期也就结束了,但是此时资源1的_next与资源2的_prev还指向着对方,因此这两个资源对应的引用计数最终都减到了1,并未减到0,导致两个节点资源仍未释放。


此时也就形成了循环引用:

  • 资源1的释放取决于资源2当中的_prev成员,而资源2的释放取决于资源1当中的_next成员。
  • 而资源1当中的_next成员的释放又取决于资源1,资源2当中的_prev成员的释放又取决于资源2,于是这就变成了一个死循环,最终导致资源无法释放。

通过weak_ptr解决循环引用问题

weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数(关键)

将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。

比如:

struct ListNode
{
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::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;
	return 0;
}

=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎

🌟~ 点赞收藏+关注 ~🌟

=========================================================================

  • 29
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

樊梓慕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值