Bootstrap

【C++】:智能指针 -- RAII思想&shared_ptr剖析


点击跳转上一篇文章: 【C++】:错误处理机制 – 异常

一,内存泄漏

(1) 什么是内存泄漏

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

(2) 内存泄漏的危害

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

void MemoryLeaks()
{
   // 1.内存申请了忘记释放
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
  
  // 2.异常安全问题
  int* p3 = new int[10];
  
  // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
  Func(); 
  
  delete[] p3;
}

二,智能指针的使用及原理

2.1 RAII思想

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

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

这种做法有两大好处

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

// 使用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;
};

int div()
{
	 int a, b;
	 cin >> a >> b;
	 if (b == 0)
	 throw invalid_argument("除0错误");
	 return a / b;
}

void Func()
{
 	Shard_Ptr<int> sp1(new int);
    Shard_Ptr<int> sp2(new int);
 	cout << div() << endl;
}

int main()
{
    try {
 	Func();
   }
    catch(const exception& e)
   {
        cout<<e.what()<<endl;
   }
 	return 0;
}

总结一下智能指针的原理

(1) RAII特性
(2) 重载operator*和opertaor->,具有像指针一样的行为

2.2 auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针auto_ptr 支持拷贝,但是拷贝时,管理权限转移,会造成被拷贝指针悬空

使用方法如下

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

	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	~Date()
	{
		cout << "~Date()" << endl;
	}
};

int main()
{
	auto_ptr<Date> ap1(new Date);

	 //拷贝时,管理权限转移,被拷贝(ap1)指针悬空
	auto_ptr<Date> ap2(ap1);
	
	// 此时ap1为空指针了,访问直接报错!
	//ap1->_year++;

	return 0;
}

结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr

2.3 unique_ptr

C++11中开始提供更靠谱的unique_ptrunique_ptr的实现原理简单粗暴的防拷贝

使用方法如下

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

	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	~Date()
	{
		cout << "~Date()" << endl;
	}
};

int main()
{
	unique_ptr<Date> up1(new Date);
	
	//不支持拷贝
	//unique_ptr<Date> up2(up1);

	return 0;
}

三,shared_ptr(重点)

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

3.1 shared_ptr的原理及使用

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

原理实现的具体细节

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

使用方法如下

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

	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	~Date()
	{
		cout << "~Date()" << endl;
	}
};

int main()
{
	shared_ptr<Date> sp1(new Date);
	shared_ptr<Date> sp2(sp1);
	shared_ptr<Date> sp3(sp2);

	return 0;
}

3.2 shared_ptr的模拟实现

1. 基本框架

namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
	
		// 构造,拷贝等其他接口.....
		
		T* get()const
		{
			return _ptr;
		}

		int use_count()const
		{
			return *_pcount;
		}

		// 重载运算符,模拟指针的行为
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr ;
		int* _pcount; //引用计数
		function<void(T* ptr)> _del = [](T* ptr) {delete ptr; };
		// function的默认构造中没有可调用对象,不给缺省值会报错
}

2. 引用计数的设计

每个资源都要配一个引用计数,是用来记录有多少个对象共同指向这块资源的。

不是每个对象都配一个计数,也不能直接使用static静态变量,这样所有对象都用一个计数了,显然不合理

所以我们要在堆上开一块空间保存计数,用一个指针指向这个计数,当每次有对象指向同一块空间时,就可以找到这个指针指向的计数++,析构时计数- -每次构造的时候就出现新资源,所以要在构造的时候申请

shared_ptr(T* ptr = nullptr)
	:_ptr(ptr)
	,_pcount(new int(1)) //每个资源给一个计数
{}

在这里插入图片描述

3. 拷贝构造

把一个对象拷贝给另一个对象,说明这个对象的资源与另一个对象共享了,计数++

// sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
	:_ptr(sp._ptr)
	, _pcount(sp._pcount)
{
	(*_pcount)++;
}

4. 析构函数

如果引用计数到0,说明已经没有对象指向这块资源了,就要释放该资源

void release()
{
	if (--(*_pcount) == 0)
	{
		//delete _ptr;
		_del(_ptr);

		delete _pcount;
		_ptr = nullptr;
		_pcount = nullptr;
	}
}

~shared_ptr()
{
	release();
}

5. 赋值拷贝

赋值拷贝是已经存在的两个对象之间。所以赋值时要注意那个对象原先资源的处理,原先的计数要先- -。并且要注意避免自己给自己赋值

// sp1 = sp3
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
	// 避免自己给自己赋值。用资源的指针判断
	// 指向同一块资源就不白费赋值
	if (_ptr != sp._ptr)
	{
		release();

		_ptr = sp._ptr;
		_pcount = sp._pcount;
		(*_pcount)++;
	}

	return *this;
}

在这里插入图片描述

3.3 shared_ptr的循环引用

循环引用问题时一个巨坑,出现时必然会导致内存泄漏问题。

struct ListNode
{
	int _data;

	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;

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

int main()
{
	//循环引用--内存泄漏
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	n1->_next = n2;
	n2->_prev = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	return 0;
}

循环引用分析图解

在这里插入图片描述

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是:weak_ptr不支持管理资源,不支持RAII。n1->_next = node2和n2->_prev = n1时,weak_ptr的_next和_prev不会增加n1和n2的引用计数

使用weak_ptr要包含头文件

#include <functional>
struct ListNode
{
	int _data;

	weak_ptr<ListNode> _next;
	weak_ptr<ListNode> _prev;

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

在我们自己的shared_ptr中也进行简单的模拟实现

template<class T>
class weak_ptr
{
public:
	weak_ptr()
	{}
	
	// 不增加引用计数
	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get())
	{}

	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();

		return *this;
	}

3.4 定制删除器

如果不是new出来的对象如何通过智能指针管理呢其实shared_ptr设计了一个删除器来解决这个问题。(ps:删除器这个问题我们了解一下)

仿函数的删除器

template <class T>
class DeleteArray
{
public:
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

class Fclose
{
public:
	void operator()(FILE* ptr)
	{
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
	}
};

int main()
{
	shared_ptr<Date[]> sp4(new Date[5]);
	shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose());

	return 0;
}

但是每次写仿函数还是有些麻烦,所以可以在shared_ptr的类中进行实现

//定制删除器
template<class D>
shared_ptr(T* ptr, D del)
	:_ptr(ptr)
	, _pcount(new int(1))
	,_del(del)
{}

3.5 shared_ptr实现的完整代码

shared_ptr.h

#pragma once
#include <functional>

namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
		// RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new int(1)) //每个资源给一个计数
		{}

		//定制删除器
		template<class D>
		shared_ptr(T* ptr, D del)
			:_ptr(ptr)
			, _pcount(new int(1))
			,_del(del)
		{}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		// sp1 = sp3
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			// 避免自己给自己赋值。用资源的指针判断
			// 指向同一块资源就不白费赋值
			if (_ptr != sp._ptr)
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				(*_pcount)++;
			}

			return *this;
		}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				//delete _ptr;
				_del(_ptr);

				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}

		~shared_ptr()
		{
			release();
		}

		T* get()const
		{
			return _ptr;
		}

		int use_count()const
		{
			return *_pcount;
		}

		// 重载运算符,模拟指针的行为
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr ;
		int* _pcount; //引用计数

		function<void(T* ptr)> _del = [](T* ptr) {delete ptr; };
		// function的默认构造中没有可调用对象,不给缺省值会报错
	};

	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
		{}

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

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();

			return *this;
		}
	private:
		T* _ptr = nullptr;
	};
}

Test.cpp

int main()
{
	bit::shared_ptr<Date> sp1(new Date);
	bit::shared_ptr<Date> sp2(sp1);

	bit::shared_ptr<Date> sp3(new Date);
	sp1 = sp3;

	//传了删除器,就用自己传的,没传就用缺省的
	bit::shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose());
	bit::shared_ptr<int> sp6((int*)malloc(40), [](int* ptr)
		{
			cout << "free:" << ptr << endl;
			free(ptr);
		});

	return 0;
}
;