【 C++ 】智能指针

news2024/9/17 8:28:05

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中的实现的。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

操作系统导论

操作系统的概念&#xff1a; 操作系统是管理计算机硬件的程序&#xff0c;它还为应用程序提供基础&#xff0c;并且充当计算机硬件和计算机用户之间的中介。 操作系统做什么&#xff1a; 计算机系统可以大致分为四个部分&#xff1a;硬件&#xff0c;操作系统&#xff0c;系…

kafka查看数据_Kafka 数据积压情况查看

由于消息消费速度处理慢或是消费端故障会导致数据产生积压。 那怎么查看数据积压量呢&#xff1f; Consumer-Groups管理&#xff1b; 在Kafka 的bin目录下提供了 kafka-consumer-groups.sh 脚本。此脚本用于管理消费情况。 查询消费者组 $KAFKA_DIR/bin/kafka-consumer-groups…

【Sql server】假设有三个字段a,b,c 以a和b分组,如何查询a和b唯一,但是c不同的记录

欢迎来到《小5讲堂》&#xff0c;大家好&#xff0c;我是全栈小5。 这是《Sql Server》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对…

深入理解计算机系统笔记

1.1 嵌套的数组 当我们创建数组的数组时&#xff0c;数组分配和引用的一般原则也是成立的。 例如&#xff0c;声明 int A[5][3]; 等价于下面的声明 typedef int row3_t[3]; row3_t A[5] 要访问多维数组的元素&#xff0c;编译器会以数组起始为基地址&#xff0c; (可能需…

教你快速认识Java中的继承和多态

目录 继承 继承的概念 继承的语法 父类成员访问 在子类方法中或者通过子类对象访问父类成员变量时&#xff1a; 在子类方法中或者通过子类对象访问父类成员方法时&#xff1a; super关键字 子类构造方法&#xff1a; 代码块执行顺序: 多态 多态的实现条件 重写 重…

MySql安全加固:配置不同用户不同账号禁止使用旧密码禁止MySql进程管理员权限

MySql安全加固&#xff1a;配置不同用户不同账号&禁止使用旧密码&禁止MySql进程管理员权限 1.1 检查是否配置不同用户不同账号1.2 检查是否禁止使用旧密码1.3 禁止MySql进程管理员权限 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496…

shell自定义日志输出函数log

Background 在编写比较复杂的脚本时&#xff0c;需要输出相关日志信息&#xff0c;方便知悉脚本的执行情况以及问题的排查。 源码 log.sh # 自定义日志函数 function log(){if [[ $1 "i" || $1 "info" ]]; thenecho -ne "\033[1;34mINFO: \033[0m&…

使用Python操作SQLite数据库

大家好&#xff0c;在数据涌现的今天&#xff0c;数据库已成为生活中不可或缺的工具。Python作为一种流行的编程语言&#xff0c;内置了多种用于操作数据库的库&#xff0c;其中之一就是SQLite。SQLite是一种轻量级的关系型数据库管理系统&#xff0c;它在Python中的应用非常广…

c++异常机制(6) -- 标准库中的异常类

标准程序库异常 我们使用c标准库中的异常类&#xff0c;来实现我们之前文件拷贝的代码。 #include <iostream> #include <stdlib.h> #include <exception> #include <ios>using namespace std;#define BUFFER_SIZE 1024// 将一个文件中的内容拷贝到另…

python中版本,操作系统等问题汇总

1. linux源码部署到windows 1.1ModuleNotFoundError: No module named pwd 这个问题&#xff0c;是因为源码是给linux的。这里在windows中&#xff0c;没有该命令。 解决方法之一&#xff0c;在相应的环境目录中&#xff0c;如图中<MetaGPTenv>虚拟环境中&#xff0c;在…

makefileGDB使用

一、makefile 1、make && makefile makefile带来的好处就是——自动化编译&#xff0c;一旦写好&#xff0c;只需要一个make命令&#xff0c;整个工程完全自动编译&#xff0c;极大的提高了软件开发的效率 下面我们通过如下示例来进一步体会它们的作用&#xff1a; ①…

今日arXiv最热大模型论文:点击即可播放!港中文发布大模型写歌神器!

一首歌&#xff0c;包含作词作曲两个部分。擅长作词or作曲就已经很牛了。比如方文山是周杰伦的御用作词人&#xff0c;而周杰伦写过很多耳熟能详的曲子。而兼具作词作曲才华的全能创作人却是难得一见。 最近港中文发布了一款歌曲创作大模型SongComposer&#xff0c;作词作曲都…

Pegasus智能家居套件样例开发--软定时器

样例简介 此样例将演示如何在Pegasus Wi-Fi IoT智能家居套件上使用cmsis 2.0 接口进行定时器开发。 工程版本 系统版本/API版本&#xff1a;OpenHarmony 3.0 releaseIDE版本&#xff1a;DevEco Device Tool Release 3.0.0.401 快速上手 准备硬件环境 预装windows系统的PC…

uniapp项目申请短信模板临时Nginx配置

现在申请短信模板不允许有变量&#xff0c;且要指向实际业务网站&#xff0c;因为项目是小程序&#xff0c;用uniapp写的&#xff0c;现在申请短信&#xff0c;把uniapp打包成h5项目&#xff0c;上传至服务器&#xff0c;修改niginx配置中springboot项目部分&#xff0c;记录一…

Netty之ChannelHandlerMask详解

Netty的ChannelHandlerMask是用于标记ChannelHandler的位掩码。它被用于指示ChannelHandler的事件处理方式。ChannelHandlerMask 定义了ChannelHandler所有事件。 final class ChannelHandlerMask {static final int MASK_EXCEPTION_CAUGHT 1;static final int MASK_CHANNEL_…

前端src中图片img标签资源的几种写法?

在 Vue 项目中引用图片路径有几种不同的方法&#xff0c;具体取决于你的项目结构和配置。以下是几种常见的方式&#xff1a; 1. 静态资源目录 (Public) 如果你的图片放在了项目的 public 目录下&#xff08;例如&#xff0c;Vite 和 Create Vue App 脚手架工具通常使用这个目…

关于页面置换算法的例题(2)

考虑一个500字的程序的下述逻辑地址访问序列&#xff1a;10&#xff0c;11&#xff0c;104&#xff0c;170&#xff0c;73&#xff0c;309&#xff0c;185&#xff0c;245&#xff0c;246&#xff0c;434&#xff0c;458&#xff0c;364。假定采用页式虚拟内存管理&#xff0c;…

力扣hot100题解(python版41-43题)

41、二叉树的层序遍历 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[9,20],[15,7]]示例…

理解C#里面的集合有哪些?怎么用,什么是安全集合?

介绍 在C#中&#xff0c;集合是一种用于存储和操作多个元素的数据结构。它们提供了各种操作&#xff0c;如添加、删除、查找等&#xff0c;以及遍历集合中的元素。集合通常根据其实现方式和行为特征进行分类。 集合继承IEnumerable 在C#中&#xff0c;几乎所有的集合类型都实现…

uniapp同步将本地图片转换为base64,支持微信、H5、APP

接上篇&#xff0c;少了一个方法的源代码。 先上代码&#xff1a; ploadFilePromiseSync (url) > { return new Promise((resolve, reject) > { // #ifdef MP-WEIXIN uni.getFileSystemManager().readFile({ filePath: url, encoding: base64, success: res > { let …