【C++】智能指针详解及原理简单说明

1、智能指针前提知识

1.1 为什么需要智能指针?

  • 在c++中进行动态内存申请的过程中,容易忘记delete
  • 即使自己没有忘记,但是因为有异常的抛出,所以也不能保证内存进行完全的释放。
  • malloc / new 申请的空间,未得到释放,造成内存泄漏
     

1.2 简单理解内存泄漏

  • 对开辟的空间未得到释放,导致应用程序对该空间失去控制
  • 频繁的开辟空间,没有得到释放,会造成内存的碎片化
     

1.3 内存泄漏的分类

  • 堆内存泄漏:程序执行开辟中new / malloc / realloc开辟的空间,没有释放
  • 系统资源泄漏:系统分配的资源,例如:套接字、文件描述符,没有释放
     

1.4 内存泄漏解决方案

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

 

2、智能指针的原理

2.1 RAII(资源获取即初始化)

利用对象生命周期来控制程序资源。

其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。

RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。

综上所述,RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放。

 

2.2 为什么要使用RAII?

RAII是用来管理资源、避免资源泄漏的方法。

在计算机系统中,资源是数量有限且对系统正常运行具有一定作用的元素。比如:网络套接字、互斥锁、文件句柄和内存等等,它们属于系统资源。由于系统的资源是有限的,所以,我们在编程使用系统资源时,都必须遵循一个步骤:

  • 1 申请资源;
  • 2 使用资源;
  • 3 释放资源。

第一步和第三步缺一不可,因为资源必须要申请才能使用的,使用完成以后,必须要释放,如果不释放的话,就会造成资源泄漏。

 

2.3 具有指针类似的行为。

例如:operator *() 、operator->()

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr) 	// 构造函数
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if(_ptr){
			delete _ptr;
			_ptr = nullptr;
		}
	}
	T& operator*() // operator*
	{
		return *_ptr;
	}
	T* operator->() // operator->
	{
		return _ptr;
	}
private:
	T* _ptr;
};

 

3、auto_ptr

3.1 auto_ptr的使用

检测: C++11中的auto_ptr使用的是资源转移实现的

void TestAuto()
{
	auto_ptr<int> ap(new int);
	auto_ptr<int> ap2(ap);

	if (ap.get() == nullptr){
		cout << "C++11中的auto_ptr使用的是资源转移实现的" << endl;
	}
}

 

3.2 拷贝问题

浅拷贝问题

  • 默认的拷贝构造函数是浅拷贝,一旦涉及到动态分配,就会出现问题。
  • 例如:多个对象指向同一块资源,在释放的时候,多个对象都对该快资源进行释放,即造成了double free的错误。

可不可以使用深拷贝?

  • 不可以。因为我们使用智能指针的目的是来帮助我们管理资源和释放资源的,没有开辟资源的权限。

 

3.3 auto_ptr 实现方式一:资源转移

  • 资源转移:使用在拷贝构造(或赋值运算符)的时候,将该块资源赋值给新创建的对象,原来的对象不能操作该块空间。即将资源转移给新的对象
  • 例如:原对象ap,新对象ap2,在创建ap2的时候用拷贝构造,那么ap的指针将被置空,ap2可以对该资源进行操作
// 实现代码
namespace test
{
	template <class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}
		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;
		}
		~auto_ptr()
		{
			if (_ptr){
				delete _ptr;
				_ptr = nullptr;
			}
		}
		T& operator*()	// 具有指针类似的行为
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T* get()  // 返回原生态的指针
		{
			return _ptr;
		}
	private:
		T *_ptr;
	};
}

 
资源转移的缺点

  • auto_ptr采用copy语义来转移指针资源,转移指针资源的所有权的同时将原指针置为NULL,这跟通常理解的copy行为是不一致的(不会修改原数据),而这样的行为在有些场合下不是我们希望看到的。
  • 例如参考《Effective STL》第8条,sort的快排实现中有将元素复制到某个局部临时对象中,但对于auto_ptr,却将原元素置为null,这就导致最后的排序结果中可能有大量的null。

 

3.4 auto_ptr 实现方式二:权限管理

  • 谁拥有这个资源的权限就能释放它
// 实现代码
namespace test
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _owner(false)
		{
			if (_ptr)
				_owner = true;
		}
		auto_ptr(auto_ptr<T>& ap)
			: _ptr(ap._ptr)
			, _owner(ap._owner)
		{
			ap._owner = false;
		}
		auto_ptr<int>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap){
				if (_ptr && ap._ptr){
					delete _ptr;
				}
				_ptr = ap._ptr;
				_owner = ap._owner;
				ap._owner = false;
			}
			return *this;
		}
		~auto_ptr()
		{
			if (_ptr && _owner){
				delete _ptr;
				_ptr = nullptr;
				_owner = false;
			}
		}
		T& operator*() // 具有指针类似的行为
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T* get()
		{
			return _ptr;
		}
		
	private:
		T* _ptr;
		// 在C++中,mutable也是为了突破const的限制而设置的
		// 被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
		mutable bool _owner;
	};
}

权限管理的缺点

  • 多个对象指向同一块资源,释放的时候只释放一次,那么我们的目的达到了。 但是其他的对象并没有置空,这样就会造成野指针,进而造成内存泄漏。

 

3.4 auto_ptr的缺点

  • 不能用于数组:因为 auto_ptr 在释放的时候,只会释放第一个空间,后序的空间没有释放,就会造成内存泄漏。
  • 不能用于非new分配的动态空间
  • 两个auto_ptr不能同时指向一个对象

因此委员会强烈建议不要auto_ptr。没有在标准库中修改是因为现在很多的项目还在使用,修改则会导致很多的问题。

 

4、unique_ptr

4.1 资源独占

采用资源独占的方式。但因为不同的资源释放的方式也不一样,所以得定制删除器。

  • 一个资源只能被一个对象管理
// 简单实现
// 定制删除器
template<class T>
class Delete
{
public:
	void operator()(T* & p)
	{
		if (p){
			delete p;
			p = nullptr;
		}
	}
};

template<class T>
class Free
{
public:
	void operator()(T* &p)
	{
		if (p){
			free(p);
			p = nullptr;
		}
	}
};

class FClose
{
public:
	void operator()(FILE* &p)
	{
		if (p){
			fclose(p);
			p = nullptr;
		}
	}
};


namespace test
{
	template<class T, class DF = Delete<T>>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}

		~unique_ptr()
		{
			if (_ptr){
				// delete _ptr; 不能写死,得用资源的类型去释放
				// 利用仿函数
				DF df;
				df(_ptr);
				_ptr = nullptr;
			}
		}

	private:
		unique_ptr(auto_ptr<T>&) = delete;
		unique_ptr<T>& operator=(auto_ptr<T>&) = delete;
	private:
		T* _ptr;
	};
}
void TestUnique()
{
	test::unique_ptr<int> up1(new int);
	test::unique_ptr<int, Free<int>> up2((int*)malloc(sizeof(int)));
	test::unique_ptr<FILE, FClose> up3(fopen("1.txt", "w"));
}

 

5、shared_ptr

5.1 shared_ptr原理

  • 引用计数:通过引用计数的方式来实现多个shared_ptr对象之间共享资源
  • 释放规则:最后一个使用资源的对象进行释放

5.2 shared_ptr使用

void TestShared()
{
	shared_ptr<int> sp1(new int);
	shared_ptr<int> sp2(sp1);
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	shared_ptr<int> sp3;
	sp3 = sp2;
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
	cout << sp3.use_count() << endl;
}

打印结果:

2
2
3
3
3

5.3 shared_ptr简单实现

namespace test
{
	template<class T>
	class Delete
	{
	public:
		void operator()(T* &p)
		{
			if (_ptr){
				delete p;
				p = nullptr;
			}
		}
	};
	template<class T, class DF = Delete<T>>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pCount(nullptr)
			, _pMutex(nullptr)
		{
			if (ptr){
				_pCount = new int(1);
				_pMutex = new mutex;
			}
		}
		~shared_ptr()
		{
			Release();
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		shared_ptr(shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pCount(sp._pCount)
			, _pMutex(sp._pMutex)
		{
			if (_ptr)
				AddRef();
		}
		shared_ptr<T>operator=(shared_ptr<T> & sp)
		{
			if (this != &sp){
				Release();

				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_pMutex = sp._pMutex;
				AddRef();
			}
			return *this;
		}
		void AddRef()
		{
			_pMutex.lock(); // 考虑线程安全问题
			++(*_pCount);
			_pMutex.unlock();
		}
		int SubRef()
		{
			_pMutex.lock();
			--(*_pCount);
			_pMutex.unlock();
			return *_pCount;
		}

	private:
		Release()
		{
			if (_ptr && 0 == SubRef()){
				DF df;
				df(_ptr);

				delete _pCount;
				_pCount = nullptr;

				delete _pMutex;
				_pMutex = nullptr;
			}
		}

	private:
		T* _ptr;
		int* _pCount;
		mutex* _pMutex;
	};
}

计数器为什么使用指针?使用静态变量可以吗?使用普通变量可以吗?

  • 不可以使用普通变量。因为普通变量每个对象都会有一个,那么也就是说在构造的时候都给计数器加 1 ,但是在释放的时候只能释放自己的计数器,导致多个对象时,计数器永远不会减到 0 的情况。
  • 不可以使用静态变量。静态变量全局只有一份,对于单个的资源的获取没有问题;但是对于多个资源的获取时,也会出现计数器永远不会减到 0 的情况。
  • 使用指针指向同一块空间,获取资源时,将该空间的计数器加 1 ;在释放时,将该空间的计数器减 1。

 
加锁的目的

  • 在 share_ptr 的内部实现时,保证引用计数的线程安全。

 

6、weak_ptr

目的:配合shared_ptr使用,解决循环引用的问题

6.1 循环引用问题

struct ListNode{
	ListNode(int x = 0)
	: left(nullptr)
	, right(nullptr)
	, data(x)
	{
		cout << "ListNode(int):" << this << endl;
	}
	~ListNode()
	{
		cout << "~ListNode():" << endl;
	}
	shared_ptr<ListNode> left;
	shared_ptr<ListNode> right;
	int data;
};

void TestShared()
{
	shared_ptr<ListNode> sp1(new ListNode(10));
	shared_ptr<ListNode> sp2(new ListNode(20));

	cout << sp1.use_count() << endl;
	cout << sp1.use_count() << endl;

	sp1->right = sp2;
	sp2->left = sp1;

	cout << sp1.use_count() << endl;
	cout << sp1.use_count() << endl;
}

打印结果:

ListNode(int):00C15058
ListNode(int):00C1A2A8
1
1
2
2

  • 我们发现打印结果没有打印析构函数中的内容,所以发生了内存泄漏

为什么会发生内存泄漏呢?

  • 上述代码执行过程
  • 创建对象sp1和sp2

在这里插入图片描述

  • sp1->right = sp2;
  • sp2->left = sp1;
  • 引用计数++
    在这里插入图片描述
  • 销毁sp1,sp2对象
  • 引用计数–
  • 引用计数不为0
  • 所以只销毁sp1和sp2指针,不销毁其指向的地方
  • 这样就会造成内存泄漏

在这里插入图片描述

6.2 循环引用问题如何解决----weak_ptr

weak_ptr

  • weak_ptr是配合shared_ptr使用的,来解决循环引用的问题
  • 注意:① weak_ptr不能独立管理资源底层也是使用引用计数的方式来实现

 
为什么weak_ptr可以解决循环引用的问题?

  • 下来看weak_ptr解决循环应用问题的过程

在这里插入图片描述
 
注意:shared_ptr在销毁对象时,use和weak都减1,而weak_ptr在销毁对象时,weak减1
 
红色的序号

  • ①销毁sp2,因为是shared_ptr,所以use和weak都减1,weak不等于0,所以不销毁引用计数
  • ②销毁left中,第③步,right没有,所以不用销毁
  • ③sp1的weak减1
  • ④销毁data
  • ⑤这步需要最后检测引用计数为0的时候才可以销毁

蓝色的序号

  • ① 销毁sp1,因为是sp1是shared_ptr,所以ues和weak减1,此时use和weak的计数都为0,所以引用计数可以销毁
  • ② 销毁right中,第③步,left没有,所以不用销毁
  • ③ 给sp2的引用计数weak减1,可以销毁这个引用计数
  • ④ 销毁data
  • ⑤ 这步在引用计数为0的时候已经销毁了

 

6.3 上述代码的改进

struct ListNode{
	ListNode(int x = 0)
	: data(x)
	{
		cout << "ListNode(int):" << this << endl;
	}

	~ListNode()
	{
		cout << "~ListNode():" << endl;
	}

	weak_ptr<ListNode> left;
	weak_ptr<ListNode> right;
	int data;
};

void TestShared()
{
	shared_ptr<ListNode> sp1(new ListNode(10));
	shared_ptr<ListNode> sp2(new ListNode(20));

	cout << sp1.use_count() << endl;
	cout << sp1.use_count() << endl;

	sp1->right = sp2;
	sp2->left = sp1;

	cout << sp1.use_count() << endl;
	cout << sp1.use_count() << endl;
}

打印结果:

ListNode(int):00BD5058
ListNode(int):00BDADF0
1
1
1
1
~ListNode():
~ListNode():

 
如果您觉得有帮助就点个赞吧!!!

学而时习之,不亦说乎!!!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值