[C++]智能指针

        本篇文章介绍一下C++里的四个智能指针auto_ptr、unique_ptr、shared_ptr还有weak_ptr,除了auto_ptr(C++98)以外,后三者是C++11后才有。它们在使用上和普通的指针差别不大(可以使用解引用 * 和箭头 -> 来访问对象),但是它们具有管理资源的功能:在智能指针对象的生命周期结束时可以自动释放所管理的资源。

智能指针原理及其引入

原理

        智能指针所用到的原理是RAII(Resource Acquisition Is Initialization),中文翻译为资源获取即初始化,是由C++之父Bjarne Stroustrup提出的。这里所指的资源可以是内存、文件句柄、网络连接、互斥量等等。其基本思路就是在申请资源时将该资源给一个对象托管,在对象的生命周期内该资源始终有效,在对象生命周期结束析构时将该资源进行释放。这样做的好处是可以不用我们人工干预释放资源的过程,也能以防我们忘记释放资源。

设计一个的智能指针(SmartPtr)

        现在我们使用RAII的思想设计一个简单的智能指针,总结为下面几个步骤:

  1. 设计一个类来封装资源。
  2. 在构造函数中初始化,在对象初始化时绑定我们要管理的资源。
  3. 在析构函数中对我们所管理的资源进行清理释放。

        我们来写一个SmartPtr类:

#include <iostream>
using namespace std;

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
private:
	T* _ptr;	//步骤一:封装资源
public:
	SmartPtr(T* ptr = nullptr)	//步骤二:在构造函数中初始化
		: _ptr(ptr)
	{}

	~SmartPtr()	//步骤三:在析构函数中清理资源
	{
		delete _ptr;
	}
};

//测试使用的A类
class A
{
public:
	int _data;
	A(int data = 0) :_data(data)
	{
		cout << "A(int data)" << endl;
	}

	~A()
	{
		cout << "~A() and its _data = " << _data << endl;
	}
};

int main()
{
	SmartPtr<A> ptr(new A(6));	//使用智能指针对获取的对象资源进行管理
	return 0;
}

        运行结果: 

 A(int data)
~A() and its _data = 6

        我们在new了一个对象后,并将其使用智能指针进行管理,这样我们就可以不显式调用delete来手动释放资源了,可以防止我们忘记手动释放资源而导致内存泄漏的情况发生。 

        为了让上面的SmartPtr的行为更像普通的指针,我们还应该重载operator*() 和 operator->()函数让其可以智能指针对象能够像普通指针一样通过*和->访问所指向的资源。下面是写了操作符重载后的代码:

#include <iostream>

using namespace std;

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
private:
	T* _ptr;	//步骤一:封装资源
public:
	SmartPtr(T* ptr = nullptr)	//步骤二:在构造函数中初始化
		: _ptr(ptr)
	{}

	~SmartPtr()	//步骤三:在析构函数中清理资源
	{
		delete _ptr;
	}

	//重载operator* 和 operator-> 来访问资源
	T& operator*() const
	{
		return *_ptr;
	}

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

	//该接口用于获得底层的指针
	T* get()	const
	{
		return _ptr;
	}
};

//测试使用的A类
class A
{
public:
	int _data;
	A(int data = 0) :_data(data)
	{
		cout << "A(int data)" << endl;
	}

	~A()
	{
		cout << "~A() and its _data = " << _data << endl;
	}
};

int main()
{
	SmartPtr<A> ptr(new A(666));	//使用智能指针对获取的对象资源进行管理

	//使用智能指针对象来访问资源
	cout << (*ptr)._data << endl;
	cout << ptr->_data << endl;

	(*ptr)._data = 777;
	cout << (*ptr)._data << endl;

	ptr->_data = 888;
	cout << ptr->_data << endl;

	return 0;
}

        运行结果:

 A(int data)
666
666
777
888
~A() and its _data = 888 

        我们下面来看C++里的4个智能指针其使用方法和功能和我们自我实现的SmartPtr大差不差,下面只讲一下它们特殊的功能和使用上需要注意的点。

一、auto_ptr

官方文档:

auto_ptr - C++ Reference (cplusplus.com)icon-default.png?t=O83Ahttps://legacy.cplusplus.com/reference/memory/auto_ptr/

auto_ptr的缺陷

        我们来看下面的代码:

#include <iostream>
#include <memory>	//包含auto_ptr的头文件

using namespace std;

//喜闻乐见的A类
class A
{
public:
	int _data;
	A(int data = 0) :_data(data)
	{
		cout << "A(int data)" << endl;
	}

	~A()
	{
		cout << "~A() and its _data = " << _data << endl;
	}
};

int main()
{
	A* ptrA = new A(10);

	auto_ptr<A> auPtr1(ptrA);	
	auto_ptr<A> auPtr2 = auPtr1;	//使用auPtr1对auPtr2进行赋值

	cout << auPtr1->_data << endl;
	cout << auPtr2->_data << endl;
	return 0;
}

        我们乍一看这段代码,auPtr1auPtr2指向同一个对象的内存空间,这很合理,我们使用auPtr1auPtr2输出一下A对象的_data时,发生了严重报错:

   

        隐隐约约的看到中间的英文:auto_ptr not dereferenceable。

       这是因为auto_ptr的赋值转移资源的特性导致的问题,这叫做管理权转移,我们将auPtr1赋值给auPtr2时,并不是像平常的指针一样,两个指针指向同一块内存空间,而是直接将auPtr1的资源直接转移托付给auPtr2管理,导致auPtr1指向的资源被置为空,而我们试图访问空指针导致的报错。

        我们来到调试窗口,分别执行下面两条语句:

	auto_ptr<A> auPtr1(ptrA);	
	auto_ptr<A> auPtr2 = auPtr1;	//使用auPtr2对auPtr1进行赋值

        调试窗口:

 

        我们发现确实如此,auPtr1在赋值给auPtr2,相当于将资源的管理权转给了auPtr2,而自身指管理的资源为空了。 

        auto_ptr的拷贝和赋值操作具有迷惑性,用不好容易导致严重错误,在日常实践中,我们应该尽量不要使用auto_ptr。 于是C++11就有了更靠谱的unique_ptr,来解决这个问题。

auto_ptr的模拟实现

//------------------------  auto_ptr  ------------------------------
// 描述:模拟实现一个auto_ptr
// 缺陷:管理权转移后旧的指针对象会被置空,继续使用会出现严重错误
//--------------------------------------------------------------------
template<class T>
class auto_ptr
{
private:
	T* _ptr;
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}

	//拷贝构造:管理权转移
	auto_ptr(auto_ptr<T>& other)
	{
		// 管理权转移
		_ptr = other._ptr;
		other._ptr = nullptr;
	}

	//赋值重载:管理权转移
	auto_ptr<T>& operator=(auto_ptr<T>& other)
	{
		// 检测是否为自己给自己赋值
		if (this != &other)
		{
			// 释放当前对象中资源
			delete _ptr;
			// 转移other中资源到当前对象中
			_ptr = other._ptr;
			other._ptr = nullptr;
		}
		return *this;
	}

	~auto_ptr()
	{
		delete _ptr;
	}

	// 重载 * 和 ->操作符
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
};

二、unique_ptr

官方文档:

unique_ptr - C++ Reference (cplusplus.com)icon-default.png?t=O83Ahttps://legacy.cplusplus.com/reference/memory/unique_ptr/?kw=unique_ptr

unique_ptr特性

        unique_ptr为了解决auto_ptr的问题,直接禁用了拷贝构造和赋值重载函数,禁止指针对象的拷贝(防拷贝),这样就不会出现auto_ptr那样导致的严重错误的情况了。在使用和功能上没有什么好讲的,和上面的SmartPtr大差不大。我们在这讲一下unique_ptr定制删除器该如何使用。

unique_ptr自定义删除器的使用

        unique_ptr默认的删除器是使用delete来清理资源的,如果我们的对象是new出来的,那么没问题,我们可以不自己制定资源的删除器,但如果我们申请的资源是通过fopen(C语言中打开文件)或者是 new[]弄出来的,则需要使用对应的fclose和delete[]来释放资源,这个时候我们需要给unique_ptr传入一个清理资源的方法,即在指针对象实例化时传入一个重载了operator()删除方法的类。

        我们先来看一下unique_ptr模板类的声明:

        下面那个是数组的特化,如果是在实例化对象时指明类型为T[]则底层会调用delete[]帮助我们释放数组资源

#define _CRT_SECURE_NO_WARNINGS 1	//解除VS对不安全函数的警告

#include <iostream>
#include <cstdio>    //包含fopen以及fclose的头文件
#include <memory>	//包含unique_ptr的头文件

using namespace std;

//喜闻乐见的A类
class A
{
public:
	int _data;
	A(int data = 0) :_data(data)
	{
		cout << "A(int data)" << endl;
	}

	~A()
	{
		cout << "~A() and its _data = " << _data << endl;
	}
};

//自定义删除器:
template<class T>
struct DeleteArr	//用于删除对应new []的对象数组
{
	void operator()(T* ptr)
	{
		delete[] ptr;
		cout << "delete[] ptr" << endl;
	}
};


struct FileClose	//用于关闭文件的FileClose
{
	void operator()(FILE* pFile)
	{
		fclose(pFile);
		cout << "fclose(pFile)" << endl;
	}
};

int main()
{
	A* pArray = new A[2];
	unique_ptr<A,DeleteArr<A>> uPtr1(pArray);	//传入删除器DeleteArr
	//或者使用下面的特化版本
	//unique_ptr<A[]> uPtr1(pArray);	//T[]的特化,调用对应delete[]

	FILE* pFile = fopen("test.txt", "w");	//打开一个文件进行写入
	unique_ptr<FILE,FileClose> uPtr2(pFile);	//传入删除器FileClose

	return 0;
}

        运行结果:

A(int data)
A(int data)
fclose(pFile)
~A() and its _data = 0
~A() and its _data = 0
delete[] ptr

 

unique_ptr的模拟实现 

//------------------------  default_delete  ------------------------------
// 描述:默认的删除器
//------------------------------------------------------------------------
template<class T>
struct default_delete
{
	void operator()(T* ptr)
	{
		delete ptr;	//默认使用delete作为删除器
	}
};

//------------------------  unique_ptr  ------------------------------
// 描述:模拟实现一个unique_ptr指针
// 特性:不能够被拷贝,解决了auto_ptr管理权转以后出现的不安全的问题
//--------------------------------------------------------------------
template<class T,class D = default_delete<T>>
class unique_ptr
{
private:
	T* _ptr;	//指向所维护的空间
public:
	unique_ptr(T* ptr) :_ptr(ptr) {}

	//禁止拷贝
	unique_ptr(const unique_ptr<T>&) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
	
	//重载 * 和 ->
	T& operator*()
	{
		return *_ptr;
	}

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

	~unique_ptr()
	{
		D del;	//删除器对象
		del(_ptr);	//使用删除器释放资源
		_ptr = nullptr;
	}
};

三、shared_ptr(共享指针)

官方文档:

shared_ptr - C++ Reference (cplusplus.com)icon-default.png?t=O83Ahttps://legacy.cplusplus.com/reference/memory/shared_ptr/?kw=shared_ptr

shared_ptr的特性和原理

1、共享指针之间允许互相拷贝,即共享指针可以像普通指针那样同时指向一个资源。

2、每个被指向的资源都有一个引用计数,记录该资源被多少个共享指针所管理,若有一个新的共享指针对象拥有该资源的管理权,那么该资源的引用计数会增加1,若有一个共享指针对象被销毁了(生命周期到了),那么就会释放对该资源的所有权,该资源的引用计数减少1。

3、如果指向该资源的最后一个共享指针对象销毁了,那么引用计数减一,引用计数等于0,此时该资源会被释放掉,归还操作系统。如果引用计数不为0,说明仍有共享指针共同管理着该资源,不能释放该资源,可以类比:生活中最后一个走出房间的人要关灯。

        下面是一个使用例子:

#include <iostream>
#include <memory>	//包含shared_ptr的头文件

using namespace std;

//喜闻乐见的A类
class A
{
public:
	int _data;
	A(int data = 0) :_data(data)
	{
		cout << "A(int data)" << endl;
	}

	~A()
	{
		cout << "~A() and its _data = " << _data << endl;
	}
};

int main()
{
	A* pA = new A(5);
	
	shared_ptr<A> sPtr1(pA);
	cout << sPtr1.use_count() << endl;	//使用共享指针里的use_count方法可以获得该内存的引用计数

	cout << endl;
	shared_ptr<A> sPtr2 = sPtr1;	//共享指针允许互相拷贝,指向同一块内存空间
	cout << sPtr1.use_count() << endl;	//使用共享指针里的use_count方法可以获得该内存的引用计数
	cout << sPtr2.use_count() << endl;	//使用共享指针里的use_count方法可以获得该内存的引用计数

	cout << sPtr1->_data << endl;
	cout << sPtr2->_data << endl;

	cout << endl;
	sPtr1->_data = 999;	//修改sPtr1所指向的数据
	cout << sPtr1->_data << endl;
	cout << sPtr2->_data << endl;

	return 0;
}

        运行结果: 

A(int data)
1

2
2
5
5

999
999
~A() and its _data = 999

shared_ptr的模拟实现 

	//------------------------  shared_ptr  ------------------------------
	// 描述:模拟实现一个共享指针,可以像普通的指针一样拷贝指向同一块空间
	// 原理:引用计数,能够知道该资源有多少个指针对象共享
	//	     当最后一个指针对象销毁时,该资源才被回收
	//--------------------------------------------------------------------
	template<class T>
	class shared_ptr
	{
	private:
		T* _ptr;	//指向共享的空间
		int* _pCount;	//引用计数,每一个共享空间对应一个计数器
		std::function<void(T*)> _del = [](T* ptr) {delete ptr; };	//函数包装:删除器,默认用delete释放

		//当一个共享指针被销毁时,对引用计数--,并判断是否需要释放所共享的空间
		void destroy()
		{
			if (--(*_pCount) == 0)
			{
				_del(_ptr);	//指向资源的最后一个共享指针析构了,使用删除器释放资源
				delete _pCount;
				_ptr = nullptr;
				_pCount = nullptr;
			}
		}
	public:
		//普通构造,默认使用delete来做删除器
		shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pCount(new int(1)) {  }

		template<class D>	//模板参数D:用来订制del删除器
		shared_ptr(T* ptr, D del) :_ptr(ptr), _pCount(new int(1)),_del(del) {  }

		//拷贝构造和赋值重载
		shared_ptr(const shared_ptr<T>& other)
		{
			_ptr = other._ptr;
			_pCount = other._pCount;
			(*_pCount)++;
		}

		//赋值重载
		shared_ptr<T>& operator=(const shared_ptr<T>& other)
		{
			destroy();	//共享指针要管理别的资源了,让原有的资源的引用计数--再指向别的空间

			_ptr = other._ptr;
			_pCount = other._pCount;
			(*_pCount)++;
			return *this;
		}

		//重载 * 和 ->
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
			
		//返回这块空间被共享指针所共享的个数
		int use_count() const
		{
			return *_pCount;
		}

		//返回底层指向内存空间的指针
		T* get() const
		{
			return _ptr;
		}

		~shared_ptr()
		{
			destroy();
		}
	};

shared_ptr自定义删除器的使用    

        shared_ptr的删除器使用方法和unique_ptr不一样 ,在unique_ptr中,删除器是以实例化unique_ptr指针对象时以具体类型传入的,而shared_ptr则是在构造函数中传入一个函数对象的方式传入的,在内部使用了包装器将这个函数对象包装起来,可在别的成员函数里使用。

        下面来看一下如何使用订制删除器:

#include <iostream>
#include <memory>	//包含shared_ptr的头文件

using namespace std;

//喜闻乐见的A类
class A
{
public:
	int _data;
	A(int data = 0) :_data(data)
	{
		cout << "A(int data)" << endl;
	}

	~A()
	{
		cout << "~A() and its _data = " << _data << endl;
	}
};

int main()
{
	A* pArr = new A[2];

	shared_ptr<A> ptr(pArr, 
		[](A* ptr) 
		{
			delete[] ptr; 
			cout << "delete[] ptr;" << endl;
		});	//传入一个带有删除方法的函数对象
	return 0;
}

        运行结果:

A(int data)
A(int data)
~A() and its _data = 0
~A() and its _data = 0
delete[] ptr;
 

 make_shared模板函数

        功能:传入构建某个对象的参数来构建一个动态开辟的对象资源,返回一个管理该资源的智能指针。

        这样做的目的是可以让为实现引用计数而动态开辟的内存紧邻着资源对象内存的前面,这样可以减少内存碎片的产生

         使用样例:

#include <iostream>
#include <memory>	//包含shared_ptr的头文件

using namespace std;

//喜闻乐见的A类
class A
{
public:
	int _data;
	A(int data = 0) :_data(data)
	{
		cout << "A(int data)" << endl;
	}

	~A()
	{
		cout << "~A() and its _data = " << _data << endl;
	}
};

int main()
{
	//传入构造A类对象需要的参数,动态开辟一个A类对象,并返回一个共享指针管理该对象资源
	shared_ptr<A> sPtr1 = make_shared<A>(10);	

	cout << sPtr1->_data << endl;
	(*sPtr1)._data++;
	cout << sPtr1->_data << endl;
	return 0;
}

        运行结果: 

A(int data)
10
11
~A() and its _data = 11

 

shared_ptr的问题:循环引用

        来看下面这个例子:

#include <iostream>
#include <memory>	//包含shared_ptr的头文件

using namespace std;

//共享指针的循环引用问题

//双向链表结点:
struct ListNode
{
	int _data;
	shared_ptr<ListNode> _pPrev;	//指向前一个头结点
	shared_ptr<ListNode> _pNext;	//指向后一个头结点

	ListNode(int data)
		:_data(data)
		,_pPrev(nullptr)
		,_pNext(nullptr)
	{
		cout << "ListNode(int data)" << endl;
	}

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

int main()
{
	//动态开辟两个结点
	shared_ptr<ListNode> pNode1(new ListNode(1));
	shared_ptr<ListNode> pNode2(new ListNode(2));

	//将这两个结点链接起来
	pNode1->_pNext = pNode2;
	pNode2->_pPrev = pNode1;

	//打印这两个结点的引用计数
	cout << pNode1.use_count() << endl;
	cout << pNode2.use_count() << endl;
	return 0;
}

        运行结果: 

ListNode(int data)
ListNode(int data)
2
2

 

        我们动态开辟的两个链表结点并没有正确的析构(程序结束时析构函数没有被调用),下面来解释一下原因。

        当智能指针pNode1pNode2刚开始接手管理数据为1和2的链表结点时,其引用计数都为1

        当我们前后连接这两个结点时,其内部的共享指针互相指向对方,数据为1和2的链表结点引用计数都增加为2

        当main函数结束时,智能指针对象pNode1pNode2销毁,数据为1和2的结点的引用计数都减少为1

        如果想要数据为1的结点析构,则需要数据为2的结点里的智能指针_pPrev销毁,即数据为2的结点析构了才能让数据为1的结点析构 。又如果想要数据为2的结点析构,则需要数据为1的结点的里的智能指针_pNext销毁,即数据为1的结点析构才能让数据为2的结点析构。这就是循环引用,一方的析构需要另外一方析构,而另外一方析构则需要这一方析构了才行,这导致了双方都无法析构。

        而循环引用的解决方法需要使用我们下面介绍的weak_ptr指针。

四、weak_ptr

官方文档:

weak_ptr - C++ Reference (cplusplus.com)icon-default.png?t=O83Ahttps://legacy.cplusplus.com/reference/memory/weak_ptr/

weak_ptr的特性

1、可以用shared_ptr的指针对象来拷贝构造一个weak_ptr对象,指向同一块资源。

2、weak_ptr的指针对象指向shared_ptr所管理的资源时,不会引起该共享资源的引用计数增加1。

3、weak_ptr会存在过期问题,如果weak_ptr指针对象所指向的资源已经没有共享指针所管理时,该weak_ptr会被视为过期,继续使用会出现问题。判断一个weak_ptr是否过期,调用weak_ptr对象里的expired()方法即可。

weak_ptr的模拟实现 

//------------------------  weak_ptr  -----------------------------------------------------------------------------------
// 描述:模拟实现一个weak指针,解决共享指针中循环引用导致无法释放资源的情况
// 该指针指向共享指针所指向的内存时,不会增加其资源的引用计数
// 但weak_ptr指针对象可能会出现过期问题,即weak_ptr指向的内存空间已经没有任何共享指针所管理了,这段内存空间被释放掉了
// 可以使用expired()方法检查一个weak_ptr指针对象是否过期,该方法实现起来复杂,下面的模拟实现并没有实现该功能
//-----------------------------------------------------------------------------------------------------------------------
template<class T>
class weak_ptr
{
private:
	T* _ptr;
public:
	weak_ptr(T* ptr = nullptr) :_ptr(ptr) {  }

	//拷贝构造和赋值重载,用共享指针给weak指针赋值,不会增加引用计数

	weak_ptr<T> operator=(const shared_ptr<T>& sharedPtr)
	{
		_ptr = sharedPtr.get();
		return *this;
	}

	weak_ptr(const shared_ptr<T>& sharedPtr)
	{
		_ptr = sharedPtr.get();
	}

	//重载 * 和 ->
	T& operator*()
	{
		return *_ptr;
	}

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

shared_ptr的循环引用问题的解决

        weak_ptr设计来只是辅助shared_ptr的,用来解决其循环引用导致的资源无法释放的问题。在上面的问题中,我们只需要将链表结点中的_pPrev和_pNext指针对象的类型从shared_ptr改为weak_ptr即可。weak_ptr只能通过默认构造、传入weak_ptr对象或者shared_ptr对象才能构造,所以不用给_pPrev和_pNext置空。

#include <iostream>
#include <memory>	//包含shared_ptr和weak_ptr的头文件

using namespace std;

//共享指针的循环引用问题

//双向链表结点:
struct ListNode
{
	int _data;
	weak_ptr<ListNode> _pPrev;	//指向前一个头结点
	weak_ptr<ListNode> _pNext;	//指向后一个头结点

	ListNode(int data)
		:_data(data)
	{
		cout << "ListNode(int data)" << endl;
	}

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

int main()
{
	//动态开辟两个结点
	shared_ptr<ListNode> pNode1(new ListNode(1));
	shared_ptr<ListNode> pNode2(new ListNode(2));

	//将这两个结点链接起来
	pNode1->_pNext = pNode2;
	pNode2->_pPrev = pNode1;

	//打印这两个结点的引用计数
	cout << pNode1.use_count() << endl;
	cout << pNode2.use_count() << endl;
	return 0;
}

        运行结果: 

ListNode(int data)
ListNode(int data)
1
1
~ListNode()
~ListNode()
 

        使用了weak_ptr后的引用计数的情况:

         

        

猜你喜欢

转载自blog.csdn.net/SUICIDEKILL/article/details/142899234