C++11标准下的智能指针

Posted AllenSquirrel

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11标准下的智能指针相关的知识,希望对你有一定的参考价值。

智能指针

为什么要提出智能指针的概念?能够解决什么问题?优点是什么?

智能指针的提出主要是解决内存泄漏的问题

那么何为内存泄漏呢?

  • 内存泄漏

内存泄漏并不是物理内存空间的丢失,而是应用程序分配某段内存空间后,由于疏忽或设计错误导致程序未能释放已经不再使用的内存空间,失去对这部分空间的控制,从而造成内存泄漏

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

  • 内存泄漏的分类

  • 堆内存泄漏(heap leak)

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

  • 系统资源泄漏

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

  • 内存泄漏解决方法

  1. 作为一位合格的程序猿或者攻城狮,工程前期就需要养成良好的代码书写规范,以及工程设计规范,凡是涉及到手动内存开辟,一定要记得及时释放,切莫置之不理。但这毕竟是一种理想状态,人为开辟内存或释放肯定会存在人为疏忽,对此,需要借助一种智能指针自动完成。
  2. 借助RALL思想完成对智能指针的设计
  3. 借助一下内存检测工具检查程序段是否存在内存泄漏

本文重点介绍通过智能指针如何解决内存泄漏

智能指针的设计主要依靠RALL思想,所谓RALL思想就是通过对象的生命周期来控制资源回收

简言之,把一份资源管理的责任托管给了对象,不再需要显示释放资源而且采用这种方式,对象所需的资源在其生命期内始终保持有效。

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

  • 常见的智能指针

  • 智能指针smart_ptr

代码如下:

#include<iostream>
using namespace std;

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }
private:
	T* _ptr;
};

struct Date
{
	int _year;
	int _month;
	int _day;
};

void test()
{
	SmartPtr<int> p(new int);
	//要求必须立即初始化,以下并非立即初始化 写法错误:
	/*int* ptr = new int;
	SmartPtr<int> p(ptr);*/
	*p = 10;
	cout << *p << endl;

	SmartPtr<Date> sparray(new Date);
	sparray->_year = 2021;
	sparray->_month = 9;
	sparray->_day = 21;
	cout << (*sparray)._year << (*sparray)._month << (*sparray)._day << endl;
	return;//return后对象生命周期结束,调用析构函数自动释放空间
	cout << "test..." << endl;
}

int main()
{
	test();
	return 0;
}

  • Auto_ptr(一般不推荐使用)

实现管理权转移,当发生拷贝或赋值过程,前面的对象就被置空,将原有对象资源转移到当前对象,这虽然解决了一块内存空间多个对象调用导致崩溃的问题,但会使得原有对象失效,后续无法再调用

代码如下:

#include<iostream>
using namespace std;

template<class T>
class AutoPtr
{
public:
	AutoPtr(T* ptr = NULL)
		: _ptr(ptr)
	{}
	~AutoPtr()
	{
		if (_ptr)
			delete _ptr;
	}
	// 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
	// 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
	AutoPtr(AutoPtr<T>& ap)
		: _ptr(ap._ptr)
	{
		ap._ptr = NULL;
	}
	AutoPtr<T>& operator=(AutoPtr<T>& ap)
	{
		// 检测是否为自己给自己赋值
		if (this != &ap)
		{
			// 释放当前对象中资源
			if (_ptr)
				delete _ptr;
			// 转移ap中资源到当前对象中
			_ptr = ap._ptr;
			ap._ptr = NULL;
		}
		return *this;
	}
	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }
private:
	T* _ptr;
};

struct Date
{
	int _year;
	int _month;
	int _day;
};

int main()
{
	AutoPtr<Date> ap(new Date);
	// 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
	// 通过ap对象访问资源时就会出现问题。
	AutoPtr<Date> copy(ap);
	ap->_year = 2020;
	return 0;
}

  • unique_ptr(基于auto_ptr存在问题而提出)

基本思想就是直接屏蔽拷贝和赋值

#include<iostream>
using namespace std;
template<class T>
class Unique_Ptr {
public:
	Unique_Ptr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	~Unique_Ptr()
	{
		if (_ptr)
			delete _ptr;
	}
	//防止拷贝delete
	Unique_Ptr(const Unique_Ptr<T>& ptr) = delete;
	Unique_Ptr<T>& operator=(const Unique_Ptr<T>& ptr) = delete;

	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }
private:
	T* _ptr;
};
  • shared_ptr

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

代码如下:

#include<iostream>
using namespace std;


template<class T>

class sharedptr
{
public:
	sharedptr(T* ptr)
		:_ptr(ptr)
		,_pRefCount(new int(1))
	{}

	sharedptr(const sharedptr<T>& s)
		:_ptr(s._ptr)
		, _pRefCount(s._pRefCount)
	{
		(*_pRefCount)++;
	}

	sharedptr<T>& operator=(const sharedptr<T>& sp)
	{
		if (this != &sp)
		{
			// 释放管理的旧资源
			if (--(*_pRefCount) == 0)   //注意:此处计数器指针与下面的计数器指针并非指向同一个  一个指向原有资源计数,另一个指向新资源计数
			{                           //赋值过程中,被赋值的对象切断原有资源指向,且原有资源计数-1,开始指向新资源,新资源计数+1
				delete _ptr;
				delete _pRefCount;
			}
			// 共享管理新对象的资源,并增加引用计数
			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			
			(*_pRefCount)++;
		}
		return *this;
	}

	~sharedptr()
	{
		if (--(*_pRefCount) == 0)
		{
			delete _ptr;
			delete _pRefCount;
		}
	}

	int UseCount() { return *_pRefCount; }

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

private:
	int* _pRefCount; // 引用计数,
	T* _ptr; // 指向管理资源的指针
};

int main()
{
	sharedptr<int> sp1(new int(10));
	sharedptr<int> sp2(sp1);
	*sp2 = 20;
	cout << sp1.UseCount() << endl;
	cout << sp2.UseCount() << endl;
	sharedptr<int> sp3(new int(10));
	sp2 = sp3;
	cout << sp1.UseCount() << endl;
	cout << sp2.UseCount() << endl;
	cout << sp3.UseCount() << endl;
	sp1 = sp3;
	cout << sp1.UseCount() << endl;
	cout << sp2.UseCount() << endl;
	cout << sp3.UseCount() << endl;
	return 0;
}

注意:

  1. 计数器定义为指针,目的是保证不同对象析构后,计数器值要同步更新
  2. 在赋值运算符重写过程中   此处计数器指针与下面的计数器指针并非指向同一个  一个指向原有资源计数,另一个指向新资源计数。赋值过程中,被赋值的对象切断原有资源指向,且原有资源计数-1,开始指向新资源,新资源计数+1

  •  shared_ptr循环引用​​​​​​​

​​​​​​​代码如下:

#include<iostream>
using namespace std;


template<class T>

class sharedptr
{
public:
	sharedptr(T* ptr)
		:_ptr(ptr)
		,_pRefCount(new int(1))
	{}

	sharedptr(const sharedptr<T>& s)
		:_ptr(s._ptr)
		, _pRefCount(s._pRefCount)
	{
		(*_pRefCount)++;
	}

	sharedptr<T>& operator=(const sharedptr<T>& sp)
	{
		if (this != &sp)
		{
			// 释放管理的旧资源
			if (--(*_pRefCount) == 0)   //注意:此处计数器指针与下面的计数器指针并非指向同一个  一个指向原有资源计数,另一个指向新资源计数
			{                           //赋值过程中,被赋值的对象切断原有资源指向,且原有资源计数-1,开始指向新资源,新资源计数+1
				delete _ptr;
				delete _pRefCount;
			}
			// 共享管理新对象的资源,并增加引用计数
			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			
			(*_pRefCount)++;
		}
		return *this;
	}

	~sharedptr()
	{
		if (--(*_pRefCount) == 0)
		{
			delete _ptr;
			delete _pRefCount;
		}
	}

	int UseCount() { return *_pRefCount; }

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

private:
	int* _pRefCount; // 引用计数,
	T* _ptr; // 指向管理资源的指针
};

struct ListNode
{
	int _data;
	sharedptr<ListNode> _prev;
	sharedptr<ListNode> _next;
	ListNode()
		:_prev(nullptr)
		,_next(nullptr)
		,_data(10)
	{}
	~ListNode() { cout << "~ListNode()" << endl; }
};

int main()
{
	sharedptr<ListNode> node1(new ListNode);
	sharedptr<ListNode> node2(new ListNode);
	cout << node1.UseCount() << endl;
	cout << node2.UseCount() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.UseCount() << endl;
	cout << node2.UseCount() << endl;
	return 0;
}

node1->_next = node2;和node2->_prev = node1;时  导致引用计数增加为2

出现问题:

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

  • 采用weak_ptr解决上述问题 

​​​​​​​解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。

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

以上是关于C++11标准下的智能指针的主要内容,如果未能解决你的问题,请参考以下文章

[C++11新特性] 智能指针详解

C++11 weak_ptr智能指针(一看即懂)

C++ - 指针和“智能指针”

C++ 智能指针最佳实践&源码分析

C++ 智能指针最佳实践&源码分析

smart_ptr之shared_ptr