C++ 智能指针 :内存泄漏、 RAII、智能指针、auto_ptr、unique_ptr、shared_ptr、weak_ptr、定制删除器deleter


内存泄漏

什么是内存泄漏

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

内存泄漏的危害:

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

一般来说,内存泄漏大多数存在于c/c++程序中,因为现在的主流语言如java,python,c#等都具有完善的垃圾回收机制,所以一般不会存在内存泄漏的情况,但也因为这种上述语言存在这种垃圾回收机制,所以在回收内存的时候也会花费宝贵的CPU资源,导致速度有所下降,所以对于c和c++,这是一把双刃剑,全靠程序员如何掌控。

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

  • 堆内存泄漏(Heap leak) 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete
    删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
  • 系统资源泄漏指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。(即使注意了释放,也可能会因为异常的抛出导致内存泄漏,需要智能指针来管理才有保证)
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具

RAII

RAII ,也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法,是利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等) 的简单技术。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源

这种利用对象的声明周期来进行资源管理的方法有以下好处

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

智能指针

智能指针就是借助RAII思想来完成的,利用智能指针的生命周期来管理指针指向的资源。

实现的思路很简单

  1. 运用RAII的原理,利用构造函数来获取数据,析构函数来销毁数据
  2. 重载操作符->和*使其操作更像指针。
template<class T>
class smart_ptr
{
public:
	smart_ptr(T* ptr)
		: _ptr(ptr)
	{}

	~smart_ptr()
	{
		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}

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

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

private:
	T* _ptr;
};

这样一个简单的智能指针雏形就完成了,他已经可以完成基本的智能指针的功能,但是还有一个问题没有解决,也就是当多个智能指针管理同一个资源的问题,如果不进行处理,就会对同一个资源进行多次释放,导致错误。

在C++中,对于这个问题,通过管理权转移、防拷贝、引用计数的方法分别实现了auto_ptr、unique_ptr、shared_ptr


auto_ptr

auto_ptr文档
在这里插入图片描述

在C++98中给出了第一个智能指针auto_ptr,他的实现思路就是管理权的转移,当通过赋值或者拷贝使两个智能指针指向同一对象时,就会将管理权从老的智能指针转到新的智能指针,此时老的智能指针就会悬空,不再指向资源。

这也是其最大的缺陷,因为被转移的指针悬空,此时访问再访问它就会导致访问空指针报错,对于不熟悉它特性的人很容易就会导致错误,所以在C++11中已经不再推荐使用auto_ptr

下面是auto_ptr的模拟实现,具体思路都在注释中

namespace lee
{
	/*
		c++98 auto_ptr
		实现思路:管理权转移
		缺陷:当管理权转移后会导致被转移的指针悬空,访问就会报错,如果不熟悉它的特性就会出问题。
	*/
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			: _ptr(ptr)
		{}

		~auto_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}
		
		//管理权转移,被转移的指针悬空
		auto_ptr(auto_ptr<T>& ap)
			: _ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				//释放当前资源
				if (_ptr)
				{
					delete _ptr;
				}

				//管理权转移
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}

			return *this;
		}

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

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

	private:
		T* _ptr;
	};
};

unique_ptr

unique_ptr文档
在这里插入图片描述
C++11中又引入了unique_ptr,他的实现思路非常简单粗暴,就是防拷贝,既然多个智能指针指向同一资源会导致问题,那就干脆不让你这样做,这样问题自然也就解决了。C++11中也是非常推荐使用这种智能指针,因为其比起shared_ptr和auto_ptr来说较为稳定,不会导致严重的错误

但是其也存在缺陷,就是在需要拷贝的场景下他没有办法使用。

下面是unique_ptr的模拟实现,具体思路都在注释中

namespace lee
{
	/*
		c++11 unique_ptr
		实现思路:防拷贝
		缺陷:对于需要拷贝的场景,他无法使用
	*/
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			: _ptr(ptr)
		{}

		//防拷贝,简单粗暴
		unique_ptr(unique_ptr<T>&) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>&) = delete;

		~unique_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}

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

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

	private:
		T* _ptr;
	};
};

shared_ptr

shared_ptr文档
在这里插入图片描述
为了弥补unique_ptr不能拷贝的缺陷,C++11中还引入了shared_ptr,他的实现思路是引用计数通过计数的方式来实现多个智能指针共同管理一个资源。

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

相较于unique_ptr,它弥补了不能拷贝的缺陷,但是因为需要保证多线程并发时的线程安全问题,所以对于计数操作要进行加锁,所以导致其效率相对来说会低一些,并且还存在循环引用的问题,所以大部分情况下如果不需要进行拷贝,都会使用unique_ptr,需要拷贝时才使用shared_ptr.

下面是shared_ptr的模拟实现,具体思路都在注释中

namespace lee
{
	/*
		c++11 shared_ptr
		实现思路:引用计数
		缺陷:会出现循环引用的问题
	*/
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _pmtx(new std::mutex)
		{}

		shared_ptr(shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pmtx(sp._pmtx)
		{
			//新增指向同一资源的指针,计数器+1
			add_ref_count();
		}

		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			//防止自己拷贝自己
			if (this != &sp)
			{
				//释放之前指向的资源
				release();

				//此时指向同一资源,计数器加一
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmtx = sp._pmtx;

				add_ref_count();
			}

			return *this;
		}

		~shared_ptr()
		{
			release();
		}

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

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

		T* get() const
		{
			return _ptr;
		}

		size_t use_count() const 
		{
			return *_pcount;
		}
	private:
		//加锁保证线程安全
		void add_ref_count()
		{
			_pmtx->lock();

			++(*_pcount);

			_pmtx->unlock();
		}

		void release()
		{
			//需要用到一个标志位,当需要释放资源时,就在解锁后把锁给释放了
			bool flag = false;

			_pmtx->lock();
			//如果为0,则释放资源
			if(--(*_pcount) == 0)
			{
				if (_ptr)
				{
					delete _ptr;
					_ptr = nullptr;
				}

				delete _pcount;
				_pcount = nullptr;
				flag = true;
			}
			_pmtx->unlock();

			if (flag == true)
			{
				delete _pmtx;
				_pmtx = nullptr;
			}
		}

		int* _pcount;
		std::mutex* _pmtx;
		T* _ptr;
	};
};

循环引用问题

例如我们用shared_ptr来管理一个双向链表节点

template<class T>
struct ListNode
{
	T data;
	struct ListNode<T>* next;
	struct ListNode<T>* prev;
};

```cpp
int main()
{
	shared_ptr<ListNode> Node1(new ListNode);
	shared_ptr<ListNode> Node2(new ListNode);
	
	Node1->_next = Node2;
	Node2->_prev = Node1;

	return 0; 
}

在这里插入图片描述

  1. 在这种情况下,当我们用智能指针分别指向Node1和Node2时,引用计数都为1。
  2. 紧接着,我们将Node1的next指向Node2,Node2的prev指向Node1,此时引用计数都为2。
  3. 当对象生命周期结束时,调用Node1和Node2的析构函数,引用计数都为1
  4. 此时问题就来了,因为next和prev还互相指向对方,所以此时资源还没有得到释放,而如果想要资源得到释放,就必须得析构掉next和prev,但是next和prev又是Node的成员,只能等待Node释放后他才能释放此时就引发了循环引用的问题,导致资源无法释放。

在c++11中,引入了weak_ptr来解决这个问题。


weak_ptr

weak_ptr文档
在这里插入图片描述
weak_ptr并不是智能指针,其没有RAII资源管理机制,他是专门用来解决shared_ptr的循环引用问题
它的实现思路就是对于会导致循环引用的地方,如上面的**Node1->_next = Node2, Node2->_prev = Node1;**这两条语句,直接进行赋值,不再进行计数

所以只需要将ListNode结构体中的指针用weak_ptr管理即可。

template<class T>
struct ListNode
{
	T data;
	struct weak_ptr<ListNode<T>> next;
	struct weak_ptr<ListNode<T>> prev;
};

下面是weak_ptr的模拟实现,具体思路都在注释中

namespace lee
{
	/*
		用于解决shared_ptr的循环引用问题
		在循环引用时直接赋值,不再计数
	*/
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr() = default;

		weak_ptr(const shared_ptr<T>& sp)
			: _ptr(sp)
		{}

		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;
	};
};

定制删除器

从上面的实现可以看到,智能指针的删除方式都是默认的调用一个delete(库中也是),但是如果我们管理的是一个文件描述符、一个数组、又或者是一个malloc出来的资源,此时就会因为无法释放资源或者资源释放不完全导致程序崩溃。
因为根据类型的不同,销毁资源的方式有很多种,所以要向正确的释放资源,就需要通过仿函数的方式,来为智能指针传递一个对应资源的释放方法,这种方法也被称为删除器,在shared_ptr和unique_ptr中也为我们提供了对应的接口。

例如以下几种常见的删除器

//默认delete
template<class T>
struct Del
{
	void operator()(T* p)
	{
		delete p;
	}
};

template<class T>
struct DelArray
{
	void operator()(T* p)
	{
		delete[] p;
	}
};

struct Free
{
	void operator()(void* p)
	{
		free(p);
	}
};
	
struct Fclose
{
	void operator()(FILE* p)
	{
		fclose(p);
	}
};

使用时根据需求传递对应删除器即可

int main()
{
	std::shared_ptr<A> sp1(new A);
	std::shared_ptr<A> sp2(new A[10], DelArray<A>());
	std::shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free());
	std::shared_ptr<FILE> sp4(fopen("test.txt", "w"), Fclose());

	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_35423154/article/details/107645109