【 C++ 】智能指针

1、内存泄漏

什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:

  • 内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。(内存泄漏是指针丢了)

内存泄漏的危害:

  • 长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
	// 2.异常安全问题
	int* p3 = new int[10];
	Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
	delete[] p3;
}

内存泄漏分类

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

堆内存泄漏(Heap leak)

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

系统资源泄漏

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

如何检测内存泄漏(了解)

  • 在linux下内存泄漏检测:linux下几款内存泄漏检测工具
  • 在windows下使用第三方工具:VLD工具说明
  • 其他工具:内存泄漏工具比较

如何避免内存泄漏

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

内存泄漏非常常见,解决方案分为两种:

  • 事前预防型。如智能指针等。
  • 事后查错型。如泄漏检测工具。

2、为什么需要智能指针

根据上面的学习我们得知内存泄漏是指因为疏忽或者错误,造成程序未能释放已经不再使用的内存的情况,如下就是一个典型的例子:

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

当b=0时,发生除0错误,此时div函数会抛一个异常,会直接跳到main函数的catch进行捕获,这就完美的错过了先前new出的p1和p2的delete释放,继而产生内存泄漏。为了解决此问题,我们推出以下两种方法:

  • 异常的重新捕获

我们可以直接在Func函数中对div函数抛出的异常直接进行捕获,在catch内部捕获时释放先前new出来的p1、p2俩资源,随后再将捕获到的异常重新抛出即可:

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

仔细看上面这段程序,难道我使用异常的重新捕获就能解决内存泄漏的问题吗?

  • 当p1抛异常时,会直接跳出去进行捕获,此时还没有new资源成功,也不需要释放,没有任何问题
  • 当p2抛异常时,会出现问题,p2抛异常会直接跳出去捕获,此时又会出现new出来的资源p1未能释放造成内存泄漏
  • 上述写的异常的重新捕获仅是针对于p2不抛异常时,针对于div抛异常而造成p1和p2未释放所作的处理,一旦p2抛了异常,根本就不会走到后续的delete那一步,直接跳出去进行捕获,从而造成p1内存泄漏
  • 可能有人说我把p2放到try{}catch(){}里面呢,这也是不可取的,因为p2抛异常时,p2还没new出来呢,还没申请资源成功怎么能进行后续的delete[] p2呢,这么做同样会有问题。何况这还只是p1和p2,如果有p3、p4……呢?你怎么知道会是哪个new抛异常呢??

由此可见,使用异常的重新捕获并不能解决所有情况,至此,我们推出智能指针的方法。

  • 智能指针(请看下文)

RAII(智能指针指导思想)

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

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

这种做法有两大好处:

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

如下我们来使用RAII的思想设计一个智能指针SmartPtr:

namespace cpp
{
	template<class T>
	class SmartPtr
	{
	public:
		//RAII思想
		SmartPtr(T* ptr)
			:_ptr(ptr)
		{}
		~SmartPtr()
		{
			cout << "delete: " << _ptr << endl;//方便观察谁释放
			delete _ptr;
		}
		//像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T* Get()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

注意我上面还实现了*和->的运算符重载,这样做是为了让其对象能够像指针一样使用,效果如下:

void Func()
{
	cpp::SmartPtr<int> sp1(new int);
	cpp::SmartPtr<pair<string, int>> sp2(new pair<string, int>("sort", 1));
	*sp1 = 0;
	sp2->second = 10;
	sp2->first = "ten";
}

现在实现好了智能指针,上述程序就不会出现内存泄漏了,调整后的代码如下:

double div()
{
	double a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	cpp::SmartPtr<int> sp1(new int);
	cpp::SmartPtr<int> sp2(new int);
	cpp::SmartPtr<int> sp3(new int);
	cpp::SmartPtr<int> sp4(new int);
	cout << div() << endl;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

正常情况下:

在这里插入图片描述

发生除0错误时:

在这里插入图片描述

此时这个程序发生除0错误时会抛异常,跳到main函数进行捕获,但此时也会出了new资源的生命周期,自动调用托管给的智能指针的析构函数来进行释放,不会出现内存泄漏问题。

当任何一个new出来的资源抛异常时,都会跳出去进行捕获,同样是先前new出来的资源出了生命周期,自动调用托管给的智能指针的析构函数来进行释放,而后面的还没new,自然不用处理,也不会出现内存泄漏问题。

智能指针的浅拷贝问题

仔细看我上面实现的智能指针,有一个很大的问题:如何拷贝?看如下的代码:

int main()
{
	cpp::SmartPtr<int> sp1(new int);
	cpp::SmartPtr<int> sp2(sp1);//拷贝构造
 
	cpp::SmartPtr<int> sp3(new int);
	cpp::SmartPtr<int> sp4(new int);
	sp3 = sp4;//拷贝赋值
	return 0;
}

此时我要拿sp1拷贝给sp2完成拷贝构造,或者是拿sp3赋值给sp3都会存在问题,导致程序崩溃,原因如下:

  • 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此sp1拷贝sp2后,相当于sp1和sp2管理了同一块内存空间,所以当sp1和sp2析构时就会导致这块空间被释放两次,程序崩溃。

在这里插入图片描述

  • 类似的,把sp4赋值给sp3时,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放。

需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是为了让这两个指针指向同一块内存空间,所以这里本就应该时浅拷贝(和迭代器那块有点像,也是浅拷贝),迭代器浅拷贝没问题的原因在于其不释放节点,而智能指针的资源是托管给我的,需要释放资源,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同类型的智能指针。详情见下文。

3、C++库里的智能指针

std::auto_ptr(不推荐)

std::auto_ptr官方文档

auto_ptr的实现原理:管理权转移的思想。

  • auto_ptr是C++98引入的智能指针,其通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这样同一个资源就不会被多次释放了,示例如下:
int main()
{
	std::auto_ptr<int> ap1(new int(1));
	std::auto_ptr<int> ap2(ap1);
    //*ap1 = 10;//ap1悬空err
	return 0;
}

下面我将通过调试的方法来演示管理权转移:

在这里插入图片描述

  • 但一个对象的管理权转移也就意味着,该对象不能再用原来管理的资源进行访问了,否则程序就会崩溃,会导致被拷贝对象(sp1)悬空。因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题,很多公司也都明确规定了禁止使用auto_ptr。下面就来模拟实现一下。

auto_ptr的模拟实现:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使auto_ptr具有像指针一样的行为。
  • 在拷贝构造函数中,用传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空。
  • 在拷贝赋值函数中,先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。
//auto_ptr的模拟实现
namespace cpp
{
	template<class T>
	class auto_ptr
	{
	public:
		//构造函数
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		//析构函数
		~auto_ptr()
		{
			if (_ptr != nullptr)
			{
				cout << "delete: " << _ptr << endl;//方便观察谁释放
				delete _ptr;
				_ptr = nullptr;
			}
		}
		//拷贝构造函数 sp2(sp1)
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
		//拷贝赋值函数
		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;
		}
		T* get()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

std::unique_ptr

C++11中开始提供更靠谱的unique_ptr

unique_ptr文档

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

  • C++11引入的unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,如果强行拷贝,那么编译就会报错,这样也能保证资源不会被多次释放。比如:
int main()
{
	std::unique_ptr<int> up1(new int);
	//std::unique_ptr<int> up2(up1);err,不能拷贝
	return 0;
}

unique_ptr的模拟实现:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使auto_ptr具有像指针一样的行为。
  • 用C++98的方式把拷贝构造函数和拷贝赋值函数只声明,不实现,并且声明为私有,或者用C++11的方式在这俩函数后面加=delete,从而防止外部调用。
//unique_ptr的模拟实现
namespace cpp
{
	template<class T>
	class unique_ptr
	{
	public:
		//构造函数
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		//析构函数
		~unique_ptr()
		{
			if (_ptr != nullptr)
			{
				cout << "delete: " << _ptr << endl;//方便观察谁释放
				delete _ptr;
				_ptr = nullptr;
			}
		}
		//*运算符重载
		T& operator*()
		{
			return *_ptr;
		}
		//->运算符重载
		T* operator->()
		{
			return _ptr;
		}
		T* get()
		{
			return _ptr;
		}
		//法一:C++11
		unique_ptr(const unique_ptr<T>& up) = delete;//删除函数
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
		//法二:C++98
	/*private:
		//1、只声明,不实现
		//2、声明成私有
		unique_ptr(unique_ptr<T>& up);//拷贝构造
		unique_ptr& operator=(unique_ptr<T>& up);//拷贝赋值*/
	private:
		T* _ptr;
	};
}

通过对unique_ptr和auto_ptr的讲解,我们得知auto_ptr虽允许拷贝,但会存在悬空的风险,有隐患,unique_ptr没有这样的问题,但是unique_ptr的功能不全,不能实现拷贝的相关功能,但是总有一些场景是需要用到拷贝的,鉴于此,我们来看下面的shared_ptr。

std::shared_ptr

shared_ptr的设计原理

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。

std::shared_ptr文档

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。详情如下:

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

通过引用计数的方式就能至此多个对象一起管理某个资源,也就支持了智能指针的拷贝,并且也只有当一个资源的引用计数减到0时才会释放资源,从而保证同一个资源不会释放多次。

int main()
{
	std::shared_ptr<int> sp1(new int);
	std::shared_ptr<int> sp2(sp1);
	std::shared_ptr<int> sp3 = sp2;
	return 0;
}

shared_ptr的模拟实现:

  • 在shared_ptr类中定义一个int*类型的成员变量_pCount在堆上,表示智能指针对象管理的资源对应的引用计数。
  • 单独写个Release释放函数,用于处理释放资源和计数的函数,将管理资源对应的引用计数–,当减到0时释放资源和计数,便于后续析构函数和拷贝赋值函数的复用。
  • 在析构函数中直接复用Release释放函数。
  • 在构造函数中获取资源,并把引用计数设为1,表示当前仅有一个资源在管理此资源。
  • 在拷贝构造函数中,一同管理传入对象的资源和计数,并且++计数,每拷贝一次,就++计数一次。
  • 在拷贝赋值函数中,先将当前对象管理的资源对应的计数–,如果减到0就要释放此资源(这个步骤可以复用Release函数),然后一同管理传入对象的资源和计数,同时对应的计数++,注意(管理同一块资源的对象之间不能进行赋值操作)。
  • 对*和->运算符进行重载,使其可以像指针一样使用。
//shared_ptr的模拟实现
namespace cpp
{
	template<class T>
	class shared_ptr
	{
	public:
		//释放函数
		void Release()
		{
			if (--(*_pCount) == 0 && _ptr)//每走一次析构,计数就--,直到计数为0时才释放管理的资源
			{
				cout << "delete: " << _ptr << endl;//方便观察谁释放
				//释放资源和计数
				delete _ptr;
				_ptr = nullptr;
				delete _pCount;
				_pCount = nullptr;
			}
		}
		//构造函数
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pCount(new int(1))//构造一个资源就把对应的计数设为1
		{}
		//析构函数
		~shared_ptr()
		{
			Release();
		}
		//拷贝构造
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
		{
			(*_pCount)++;//每拷贝一个对象就对计数++
		}
		//拷贝赋值sp1 = sp3
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)//管理同一块资源的对象之间不能进行赋值操作
			{
				Release();//需要先--sp1的计数,因为sp1托管其它资源了,减去自己原先的计数,并且计数减到0时释放sp1之前管理的资源,统一放到Release函数处理
				_ptr = sp._ptr;//把sp3的资源赋给sp1
				_pCount = sp._pCount;//把sp3的计数赋给sp1
				++(*_pCount);//此时sp3和sp1共同托管sp3的资源,相应的计数++
			}
			return *this;
		}
		//*运算符重载
		T& operator*()
		{
			return *_ptr;
		}
		//->运算符重载
		T* operator->()
		{
			return _ptr;
		}
		T* get() const
		{
			return _ptr;
		}
		int use_count()
		{
			return *_pCount;
		}
	private:
		T* _ptr; //管理的资源
		int* _pCount;//管理的资源对应的引用计数
	};
}

下面通过调试来演示下shared_ptr完成拷贝构造和拷贝赋值的过程

拷贝构造:

在这里插入图片描述

拷贝赋值:

在这里插入图片描述

引用计数一定是int*类型的指针(在堆区)。下面解释原因:

首先,shared_ptr中的引用计数_pCount不能是int出来的,这会导致管理同一块资源的shared_ptr对象有不同的引用计数,而多个对象管理统一块资源的本质是其资源和引用计数变量完全一致,因此不能使用int出来的_pCount,图示如下:

在这里插入图片描述

shared_ptr的引用计数_pCount也不能是静态成员变量,因为静态成员变量是所有类型对象共享的,这回导致管理不同资源的对象的引用计数都是一样的,而不同资源的对象的引用计数应该是不同的。

在这里插入图片描述

这里只能把shared_ptr的引用计数_pCount设定为指针, 当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的指针,如果有其它对象也要管理此资源,那么资源和引用计数都要赋给他,此时管理同一个资源的多个对象访问到的就是同一个引用计数,管理不同资源的对象访问的就不是同一个引用计数,相当于将各个资源与其对应的计数进行了绑定。注意后续释放资源的时候也要把此在堆区的计数变量释放掉。

在这里插入图片描述

shared_ptr的线程安全问题

std::weak_ptr

shared_ptr的循环引用问题

shared_ptr的循环引用问题只有在特定情况下才会出现,看如下的示例(定义一个双向的节点类,在析构函数中以打日志的方式输出一条提示语句,告知我们节点有无释放,在main函数中new出p1和p2两个节点,并把p1和p2建立双向链接关系,最后delete释放俩节点)。

struct ListNode
{
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	int _val = 0;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	ListNode* p1 = new ListNode;
	ListNode* p2 = new ListNode;
	p1->_next = p2;
	p2->_prev = p1;
	delete p1;
	delete p2;
	return 0;
}

此段程序是没有问题的,new出的p1和p2都能正常释放:

在这里插入图片描述

假设现在出现种种原因导致程序抛异常,以至于内存泄漏,我们现在需要将其放入智能指针(shared_ptr)里头,让其托管资源并帮助我们释放资源,注意要把ListNode节点类中的_prev和_next指针也改为shared_ptr类型,便于后续的节点赋值操作。

struct ListNode
{
	std::shared_ptr<ListNode> _next = nullptr;
	std::shared_ptr<ListNode> _prev = nullptr;
	int _val = 0;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> p1(new ListNode);
	std::shared_ptr<ListNode> p2(new ListNode);
	p1->_next = p2;
	p2->_prev = p1;
	return 0;
}

此时程序会出现一个严重的问题:内存泄漏。出错的地方在p1和p2建立链接关系那,如果我注释掉链接节点中的任意一个代码,程序都不会出现任何问题,两个节点最后都能释放,造成此现象的原因就是shared_ptr的循环引用,下面画图演示。

首先,我new了两个节点,并将这俩节点的资源托管给两个shared_ptr智能指针,此时这俩资源对应的引用计数均为1。

在这里插入图片描述

接着执行p1->_next = p2; p2->_prev = p1; 建立好了链接关系后,资源1的_next成员和p2一同管理资源2,资源2中的_prev成员和p1一同管理资源1,此时资源1和资源2对应的引用计数都增加到了2。

在这里插入图片描述

当出了main函数作用域时,p1和p2的声明周期也就结束了,节点p2和p1相继调用析构函数释放了,此时资源1和资源2的引用计数相应的–到1。

在这里插入图片描述

下面来解释为何会出现循环引用以至于节点未释放内存泄漏的原因:

  • 此时管理资源2的是资源1中的_next指针,_next释放,资源2才释放,类似的管理资源1的是资源2中的_prev指针,_prev释放,资源1才释放。
  • 只有资源对应的引用计数减到0时资源才会释放。

总结:

  • 资源2的_prev管着左边的节点资源1,资源1的_next管着右边的节点资源2,它们分别是两个节点的自定义成员,只有节点释放,成员才析构释放。
  • 所以,左边节点资源1释放,资源2的_next才析构,左边节点的释放又依赖于右边节点资源2的_prev,_prev析构,左边节点才释放,而右边节点的_prev想要析构,又取决于右边节点的释放,而右边节点想要释放,又依赖于左边节点的_next……

上述无限套娃的问题就是shared_ptr智能指针中典型的循环引用问题。但在一开始我说到p1和p2的链接关系但凡去掉一个,p1和p2就都能正常释放,下面画图演示:

  • 首先,我new了两个节点,并将这俩节点的资源托管给两个shared_ptr智能指针,此时这俩资源对应的引用计数均为1。
    在这里插入图片描述
  • 接着执行p1->_next = p2;此时资源1的_next成员和p2一同管理资源2,资源2的引用计数为2,资源1仅是被p1管理,引用计数仍为1。
    在这里插入图片描述
  • 当出了main函数作用域时,p1和p2的声明周期也就结束了,节点p2和p1相继调用析构函数释放了,此时资源1和资源2的引用计数相应的–,资源1的引用计数为0,资源2的引用计数为1。
    在这里插入图片描述

当资源1的引用计数减到0时,此节点就释放了,此时资源1的_next也就析构释放了,但是_next又管理者资源2,当资源1的_next释放了,节点2对应的资源也就释放了,相应的引用计数–到0。此时节点正常释放,截图运行结果如下:

在这里插入图片描述

当然实际工程中,我不可能说想要链接p1和p2还只留一个,这样何谈链接,虽然对于shared_ptr,只有这样才能避免出现循环引用问题的内存泄漏,为了解决上述出现的循环引用问题,于是库里推出了weak_ptr智能指针。

weak_ptr解决shared_ptr的循环引用问题

weak_ptr可以接收一个shared_ptr的对象。拷贝shared_ptr的对象,进行辅助管理,weak_ptr解决shared_ptr的循环引用问题的原理就是,p1->_next = p2;和p2->_prev = p1;时weak_ptr的_next和_prev不会增加p1和p2的引用计数。

在这里插入图片描述

我们把ListNode节点类中的_next和_prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时p1和p2节点的声明周期结束时,两个资源对应的引用计数减到0,继而可释放两个节点的资源。如下的示例:(下面的use_count函数专门用于输出当前的引用计数)

struct ListNode
{
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val = 0;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	std::shared_ptr<ListNode> p1(new ListNode);
	std::shared_ptr<ListNode> p2(new ListNode);
	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;
	p1->_next = p2;
	p2->_prev = p1;
	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;
	return 0;
}

在这里插入图片描述

通过use_count函数获取的这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数都是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数。由此可见,weak_ptr解决了shared_ptr的循环引用问题。

weak_ptr的模拟实现:

  • 提供一个无参的构造函数。
  • 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时仅获取shared_ptr对象管理的资源。
  • 支持用shared_ptr对象拷贝赋值weak_ptr对象,赋值时仅获取shared_ptr对象管理的资源。
  • 对*和->运算符重载,使weak_ptr可以像指针一样使用。
//weak_ptr的模拟实现
namespace cpp
{
	template<class T>
	class weak_ptr
	{
	public:
		//无参构造函数
		weak_ptr()
			:_ptr(nullptr)
		{}
		//用shared_ptr对象拷贝构造weak_ptr对象
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}
		//用shared_ptr对象拷贝赋值weak_ptr对象
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp.get())
			{
				_ptr = sp.get();
			}
			return *this;
		}
		//*运算符重载
		T& operator*()
		{
			return *_ptr;
		}
		//->运算符重载
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr; //管理的资源
	};
}

weak_ptr不参与指向资源的释放管理,weak_ptr的意义在于解决shared_ptr的循环引用问题。

4、定制删除器

上述我们讲解的unique_ptr和shared_ptr都会面临一个巨大的问题,上述所有的智能指针都是默认以delete _ptr的方式进行释放资源,但是智能指针并不只是管理new出来的资源,万一我是new [ ]、malloc、文件指针……,下面分别讨论unique_ptr和shared_ptr对应的定制删除器。

unique_ptr中的定制删除器

看如下我有三种不同方式申请的资源:

class Date
{
public:
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
int main()
{
	std::unique_ptr<Date> up1(new Date[10]);//err
	std::unique_ptr<Date> up2((Date*)malloc(sizeof(Date) * 10));//err
	std::unique_ptr<FILE> up3((FILE*)fopen("Test.cpp", "r"));//err
	return 0;
}

此时当智能指针对象的声明周期结束时,再像先前那样一味的使用delete的方式释放资源就会出现程序崩溃,对于new [ ],应该用delete[ ]的方式释放,对于malloc的,应该用free的方式释放。

为了解决上述问题,C++中的unique_ptr在模板参数中以传仿函数类型的方式传入的删除器(只需掌握下图中的第一种即可):

在这里插入图片描述

由此可见,我们只需要对不同方式申请的资源写上对应的仿函数完成其需要的释放方式即可,如下:

class Date
{
public:
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
//针对new[]的释放
template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};
//针对malloc的释放
template<class T>
struct Free
{
	void operator()(T* ptr)
	{
		cout << "free" << ptr << endl;
		free(ptr);
	}
};
//针对fopen的释放
struct Fclose
{
	void operator()(FILE* ptr)
	{
		cout << "fclose" << ptr << endl;
		fclose(ptr);
	}
};
int main()
{
	std::unique_ptr<Date, DeleteArray<Date>> up1(new Date[10]);
	std::unique_ptr<Date, Free<Date>> up2((Date*)malloc(sizeof(Date) * 10));
	std::unique_ptr<FILE, Fclose> up3((FILE*)fopen("Test.cpp", "r"));
	return 0;
}

在这里插入图片描述

下面来调整我们自己实现的unique_ptr,使其支持定制删除器。

  • C++中的unique_ptr在模板参数中以传仿函数类型的方式传入的删除器。
  • 针对于unique_ptr的析构函数,构造传入仿函数的对象,传入对应的指针,调用相应的仿函数完成对应的类型的释放即可。
//unique_ptr的完整模拟实现
namespace cpp
{
	//默认类型的释放,针对单纯的new资源的释放
	template<class T>
	struct default_delete
	{
		void operator()(T* ptr)
		{
			delete ptr;
		}
	};
	//针对new[]的释放
	template<class T>
	struct DeleteArray
	{
		void operator()(T* ptr)
		{
			cout << "delete[]" << ptr << endl;
			delete[] ptr;
		}
	};
	//针对malloc的释放
	template<class T>
	struct Free
	{
		void operator()(T* ptr)
		{
			cout << "free" << ptr << endl;
			free(ptr);
		}
	};
	//针对fopen的释放
	struct Fclose
	{
		void operator()(FILE* ptr)
		{
			cout << "fclose" << ptr << endl;
			fclose(ptr);
		}
	};
	template<class T, class D = default_delete<T>>//模板参数中传入仿函数类型
	class unique_ptr
	{
	public:
		//构造函数
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		//析构函数
		~unique_ptr()
		{
			if (_ptr != nullptr)
			{
				//cout << "delete: " << _ptr << endl;//方便观察谁释放
				//delete _ptr;
				D del;
				del(_ptr);
				_ptr = nullptr;
			}
		}
		//*运算符重载
		T& operator*()
		{
			return *_ptr;
		}
		//->运算符重载
		T* operator->()
		{
			return _ptr;
		}
		T* get()
		{
			return _ptr;
		}
        /*防止拷贝的两种方法*/
		//法一:C++11
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
		//法二:C++98
	/*private:
		//1、只声明,不实现
		//2、声明成私有
		unique_ptr(unique_ptr<T>& up);//拷贝构造
		unique_ptr& operator=(unique_ptr<T>& up);//拷贝赋值*/
	private:
		T* _ptr;
	};
}

此时unique_ptr智能指针就不再害怕不是new出来的资源了。下面我们就来看看shared_ptr中的定制删除器。

shared_ptr的定制删除器

shared_ptr中的定制删除器不同于unique_ptr中的定制删除器,shared_ptr是在构造函数中支持的,而unique_ptr是在模板参数中支持的:

在这里插入图片描述

参数说明:

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

示例如下:

//针对new[]的释放
template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};
//针对malloc的释放
template<class T>
struct Free
{
	void operator()(T* ptr)
	{
		cout << "free" << ptr << endl;
		free(ptr);
	}
};
//针对fopen的释放
struct Fclose
{
	void operator()(FILE* ptr)
	{
		cout << "fclose" << ptr << endl;
		fclose(ptr);
	}
};
int main()
{
	//针对new的释放
	cpp::shared_ptr<Date> sp1(new Date);//默认new的释放
	//针对new[]的释放
	std::shared_ptr<Date> sp2(new Date[10], DeleteArray<Date>());//传仿函数的匿名对象释放
	std::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr; });//传lambda表达式释放
	//针对malloc的释放
	std::shared_ptr<Date> sp4((Date*)malloc(sizeof(Date) * 10), Free<Date>());//传仿函数的匿名对象释放
	//针对fopen的释放
	std::shared_ptr<FILE> sp6((FILE*)fopen("Test.cpp", "r"), Fclose());//传仿函数的匿名对象释放
	std::shared_ptr<FILE> sp5((FILE*)fopen("Test.cpp", "r"), [](FILE* ptr) {
		cout << "fclose: " << ptr << endl;
		fclose(ptr); });传lambda表达式释放
	return 0;
}

注意:这里传对象作为定制删除器除了可以传仿函数,也可以传我们之前学过的lambda表达式,如上述代码所示。

5、C++11和boost中智能指针的关系

  • C++ 98 中产生了第一个智能指针auto_ptr。
  • C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr。
  • C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  • C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/419834.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【python报错】Intel MKL FATAL ERROR: Cannot load mkl/../../../libmkl_rt.so.2.

python报错&#xff1a; Intel MKL FATAL ERROR: Cannot load mkl/../../../libmkl_rt.so.2.在切换旧版numpy版本的时候&#xff0c;出现了这个报错&#xff0c;表现就是将numpy切换到<1.24的版本的时候&#xff0c;只要import numpy就弹出以上报错。 尝试了网上的各种方法…

Socket网络编程(五)——TCP数据发送与接收并行

目录 主要实现需求TCP 服务端收发并行重构启动main方法重构重构分离收发消息的操作重构接收消息的操作重构发送消息TCPServer调用发送消息的逻辑监听客户端链接逻辑重构Socket、流的退出与关闭 TCP 客户端收发并行重构客户端 main函数重构客户端接收消息重构客户端发送消息重构…

Python程序的流程

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 年轻是我们唯一拥有权利去编制梦想的时…

【Java程序设计】【C00324】基于Springboot的高校疫情防控管理系统(有论文)

基于Springboot的高校疫情防控管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的高校疫情防控系统&#xff0c;本系统有管理员、学校管理员、学院管理员、教师以及学生五种角色&#xff1b; 管理员&#x…

智慧灌区项目案例(甘肃省兰州市某重点灌区)

​甘肃省兰州市某重点灌区自上个世纪80年代建成后,灌溉面积达到30万亩,对推动当地农业发展发挥了重要作用。但长期以来,该灌区的水利管理仍主要依靠人工统计记录,缺乏实时监测和精细化管理。为实现灌区管理的现代化升级,甘肃水利局委托星创易联公司设计实施水利信息化项目。 项…

CSAPP-信息的表示和处理

文章目录 概念扫盲思想理解经典好图安全事件 概念扫盲 1.大端高位在前&#xff0c;小端低位在前 2.逻辑运算符&#xff08;&& 、||、&#xff01;&#xff09;与位级运算&#xff08;&、|、~&#xff09;的差异 3.宏可以保证无论代码如何编译&#xff0c;都能生成…

DSP软件架构

&#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;计算机杂记 &#x1f380;CSDN主页 发狂的小花 &#x1f304;人生秘诀&#xff1a;学习的本质就是极致重复! 目录 一 数字信号处理基本运算 二 DSP软件架构 1 哈…

谷歌最强开源大模型亮相!Gemini技术下放,笔记本就能跑,可商用

明敏 发自 凹非寺 量子位 | 公众号 QbitAI 谷歌大模型&#xff0c;开源了&#xff01; 一夜之间&#xff0c;Gemma系列正式上线&#xff0c;全面对外开放。 它采用Gemini同款技术架构&#xff0c;主打开源和轻量级&#xff0c;免费可用、模型权重开源、允许商用&#xff0c;…

【前端素材】推荐优质后台管理系统cassie平台模板(附源码)

一、需求分析 1、系统定义 后台管理系统是一种用于管理网站、应用程序或系统的管理界面&#xff0c;通常由管理员和工作人员使用。它提供了访问和控制网站或应用程序后台功能的工具和界面&#xff0c;使其能够管理用户、内容、数据和其他各种功能。 2、功能需求 后台管理系…

Redis 缓存数据库

redis 中文网 http://www.redis.cn/ redis.net.cn 两种数据库阵营 1.关系型数据库 MySQL Oracle DB2 SQL Server 等基于二维表结构存储数据的文件型磁盘数据库 缺点: 因为数据库的特征是磁盘文件型数据库, 就造成每次查询都有IO操作, 海量数据查询速度较慢 2.NoSQL数据库 …

[技巧]Arcgis之图斑四至范围计算

ArcGIS图层&#xff08;点、线、面三类图形&#xff09;四至范围计算 说明&#xff1a;如下图画出来的框&#xff08;范围标记不是很准&#xff09; &#xff0c;图斑的x最大和x最小&#xff0c;y最大&#xff0c;y最小值则为四至范围值&#xff0c;通俗的讲就是图斑的最小外接…

微信开发者工具-代码管理和码云Github远程仓库集成

目录 思考&#xff1a;IDE如何进行代码管理 代码管理方式 一、自身提供服务 二、Git 扩展 1、环境准备 2、创建项目代码 3、进行项目Git初始化 4、在码云新建远程仓库 5、将项目进行远程仓库关联 三、SVN扩展 四、代码管理 思考&#xff1a;IDE如何进行代码管理 初识开…

力扣2月最后三天的每日一题

力扣2月最后三天的每日一题 前言2867.统计树中的合法路径数目思路确定1e5中的质数统计每个点的连接情况开始对质数点进行处理完整代码 2673.使二叉树所有路径值相等的最小代价思路完整代码 2581.统计可能的树根数目思路建立连通关系将猜测数组变为哈希表&#xff0c;方便查询利…

高级语言期末2007级B卷(计算机学院)

1.从键盘输入任意一个整数&#xff0c;求此整数各位数字中零的个数&#xff0c;以及各位数字中最大者。 #include <stdio.h>int getzero(char *str){char max0;int i0;int count0;while(str[i]!\0){if(str[i]>max)maxstr[i];if(str[i]0)count;i;}printf("%c\n&q…

vue如何重写移动端长按文字复制的功能

移动端长按文字会出现 “复制 全选”的默认弹框&#xff08;这里拿安卓举例吧&#xff09; 但是有的时候需要在长按的时候增加别的功能 这时候就需要禁用原生的弹框然后重写自己的功能 第一步&#xff1a;禁用掉原生弹窗 但是支持划选文字 重要css属性&#xff1a; -webkit…

HarmonyOS Full SDK的安装

OpenHarmony的应用开发工具HUAWEI DevEco Studio现在随着OpenHarmony版本发布而发布,只能在版本发布说明中下载,例如最新版本的OpenHarmony 4.0 Release。对应的需要下载DevEco Studio 4.0 Release,如下图。 图片 下载Full SDK主要有两种方式,一种是通过DevEco Studio下载…

【毛毛讲书】【时间贫困】时间都去哪了?

重磅推荐专栏&#xff1a; 《大模型AIGC》 《课程大纲》 《知识星球》 本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域&#xff0c;包括但不限于ChatGPT和Stable Diffusion等。我们将深入研究大型模型的开发和应用&#xff0c;以及与之相关的人工智能生成内容&#xff…

查看网络连接的netstat

netstat是一个监控TCP/IP网络的非常有用的工具&#xff0c;可以显示路由表、实际的网络连接&#xff0c;以及每一个网络接口设备的状态信息&#xff0c;可以让用户得知目前都有哪些网络连接正在运作。netstat用户显示与IP、TCP、UDP和ICMP协议相关的统计数据&#xff0c;一般用…

文件怎么减小内存?4个简单的方法~

随着我们在电脑或移动设备上创建、下载和收集越来越多的文件&#xff0c;存储空间的管理变得尤为重要。有时&#xff0c;文件太大会占用过多的内存&#xff0c;导致存储空间不足的问题。但别担心&#xff0c;本文将向您介绍五种简单有效的方法&#xff0c;帮助您轻松减小文件的…

微信云开发-- Mac安装 wx-server-sdk依赖

第一次上传部署云函数时&#xff0c;会提示安装依赖wx-server-sdk 一. 判断是否安装wx-server-sdk依赖 先创建一个云函数&#xff0c;然后检查云函数目录。 如果云函数目录下只显示如下图所示三个文件&#xff0c;说明未安装依赖。 如果云函数目录下显示如下图所示四个文件&a…