C++之智能指针

智能指针

  • 为什么需要智能指针?
  • 内存泄漏
    • 什么是内存泄漏,内存泄漏的危害
    • 内存泄漏分类
    • 如何避免内存泄漏
  • 智能指针的使用及原理
    • RAII
    • 智能指针的原理
    • C++中的智能指针
      • std::auto_ptr
      • std::unique_ptr
      • std::shared_ptr
      • std::shared_ptr的线程安全问题
      • std::shared_ptr的循环引用
      • std::weak_ptr
      • std::shared_ptr的定制删除器
  • C++11和boost中智能指针的关系

为什么需要智能指针?

首先,我们来看一段代码:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	int* p1 = new int;
	int* p2 = new int;

	cout << div() << endl;

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

我们会发现,上述代码中,在div函数中抛出了一个异常,此时就会直接跳转至main函数的catch位置,p1和p2此时new出来的空间并没有得到释放,就会造成内存泄漏。

而我们如果要解决这个问题,就要在p1,p2开辟空间的位置分别抛出异常,然后在catch(…)接收,这样就显得特别的麻烦,所以也就有了智能指针;

内存泄漏

什么是内存泄漏,内存泄漏的危害

什么是内存泄漏

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

内存泄漏的危害

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

内存泄漏分类

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

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

如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证;
  2. 采用RAII思想或者智能指针来管理资源;
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项;
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵;

总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

智能指针的使用及原理

RAII

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

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

  • 不需要显式地释放资源;
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
template<class T>
class Smartptr
{
public:
	//构造函数
	Smartptr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	
	//析构函数
	~Smartptr()
	{
		cout << "delete[]:" << _ptr << endl;
		if(_ptr)
			delete _ptr;
	}

private:
	T* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{

	Smartptr<int> sp1(new int);
	Smartptr<int> sp2(new int);

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

代码中将申请到的内存空间交给了一个SmartPtr对象进行管理:

  • 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来;
  • 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放。

智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将*->重载下,才可让其像指针一样去使用。

template<class T>
class Smartptr
{
public:
	//构造函数
	Smartptr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	
	//析构函数
	~Smartptr()
	{
		cout << "delete[]:" << _ptr << endl;
		if(_ptr)
		delete _ptr;
	}

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

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

private:
	T* _ptr;
};

我们的智能指针还需要解决对象的拷贝问题,我们来看下面这段代码:

int main()
{
	Smartptr<int> sq1(new int);
	Smartptr<int> sq2(sq1);//拷贝构造

	Smartptr<int> sq3(new int);
	Smartptr<int> sq4(new int);
	sq3 = sq4;//拷贝赋值

	return 0;
}

我们会发现对于内置类型发生的都是浅拷贝,而上述代码中就会出现同一块空间被释放两次,或者是失去对一块空间的控制,没有进行是否而造成内存泄漏。

而我们的智能指针作为一个原生指针,就是想让两个指针指向同一片空间,本就应该是浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。

C++中的智能指针

std::auto_ptr

管理权转移

auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。

int main()
{
	std::auto_ptr<int> sq1(new int);
	std::auto_ptr<int> sq2(sq1);

	std::auto_ptr<int> sq3(new int);
	std::auto_ptr<int> sq4(new int);
	sq3 = sq4;//拷贝赋值

	return 0;
}

但是我们需要注意的是管理权转移以后,我们就不能对对原来的资源进行访问了,否则程序就会崩溃,因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题:
C++之智能指针_第1张图片

auto_ptr的模拟实现

auto_ptr的实现步骤如下:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • *->运算符进行重载,使auto_ptr对象具有指针一样的行为。
  • 在拷贝构造函数中,用传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空。
  • 在拷贝赋值函数中,先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。
namespace gtt {
	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& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}

			return *this;
		}
		//析构函数
		~auto_ptr()
		{
			cout << "delete[]:" << _ptr << endl;
			if (_ptr)
				delete _ptr;
		}

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

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

	private:
		T* _ptr;
	};
}

int main()
{
	gtt::auto_ptr<int> sq1(new int);
	gtt::auto_ptr<int> sq2(sq1);

	gtt::auto_ptr<int> sq3(new int);
	gtt::auto_ptr<int> sq4(new int);
	sq3 = sq4;//拷贝赋值

	return 0;
}

std::unique_ptr

防拷贝

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

int main()
{
	std::unique_ptr<int> sq1(new int);
	//std::unique_ptr sq2(sq1); error

	std::unique_ptr<int> sq3(new int);
	std::unique_ptr<int> sq4(new int);
	//sq3 = sq4; error

	return 0;
}

unique_ptr的模拟实现:

  • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
  • 对*和->运算符进行重载,使unique_ptr对象具有指针一样的行为。
  • 用C++98的方式将拷贝构造函数和拷贝赋值函数声明为私有,或者用C++11的方式在这两个函数后面加上=delete,防止外部调用。
namespace gtt {
	template<class T>
	class unique_ptr
	{
	public:
		//构造函数
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

		//析构函数
		~unique_ptr()
		{
			cout << "delete[]:" << _ptr << endl;
			if (_ptr)
				delete _ptr;
		}

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

		T* operator->()
		{
			return _ptr;
		}
		
		//防拷贝
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr& operator=(const unique_ptr<T>& up) = delete;
	private:
		T* _ptr;
	};
}

std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptrshared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源:

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

	std::shared_ptr<int> sq3(new int);
	std::shared_ptr<int> sq4(new int);
	sq3 = sq4;  
	cout << sq3.use_count() << endl;//2

	return 0;
}

通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。
C++之智能指针_第2张图片

  • 在shared_ptr类中增加一个成员变量count,表示智能指针对象管理的资源对应的引用计数。
  • 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前只有一个对象在管理这个资源。
  • 在拷贝构造函数中,与传入对象一起管理它管理的资源,同时将该资源对应的引用计数++。
  • 在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数–(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。
  • 在析构函数中,将管理资源对应的引用计数–,如果减为0则需要将该资源释放。
  • 对*和->运算符进行重载,使shared_ptr对象具有指针一样的行为。

首先,shared_ptr中的count并不能定义为一个普通的int类型的变量,因为定义成一个普通的int就会导致每一个shared_ptr对象都拥有一个自己的count,我们需要的是多个对象管理同一个资源,所以要是同一个引用计数,如下图所示:
C++之智能指针_第3张图片
同样,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数,如下图:
C++之智能指针_第4张图片
而如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。

这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。
C++之智能指针_第5张图片
需要注意,由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。

namespace gtt {
	template<class T>
	class shared_ptr
	{
	public:
		//构造函数
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_count(new int(1))
		{}

		//析构函数
		~shared_ptr()
		{
			if (--(*_count) == 0)
			{
				if (_ptr != nullptr)
				{
					cout << "delete[]:" << _ptr << endl;
					delete _ptr;
					_ptr = nullptr;
				}
				delete _count;
				_count = nullptr;
			}
		}

		//拷贝构造函数
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _count(sp._count)
		{
			(*_count)++;
		}

		//拷贝赋值函数
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				if (--(*_count) == 0)
				{
					cout << "delete[]:" << _ptr << endl;
					delete _ptr;
					delete _count;
				}

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

			return *this;
		}

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

		T* operator->()
		{
			return _ptr;
		}
		//获取引用计数
		int use_count()
		{
			return *_count;
		}
		
		T* get()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _count;
	};
}

std::shared_ptr的线程安全问题

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2。这样引用计数就错乱了,会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++--是需要加锁的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

比如下面这段程序,用一个shared_ptr管理一个整型变量,然后用两个线程分别对这个shared_ptr对象进行1000次拷贝操作,这些对象被拷贝出来后又会立即被销毁。

struct Data
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void func(gtt::shared_ptr<Data>& sp, int n)
{
	for (int i = 0; i < n; i++)
	{
		shared_ptr<Data> copy(sp);
	}
}
void test()
{
	gtt::shared_ptr<Data> p(new Data);
	cout << p.get() << endl;

	const size_t n = 10000;
	thread t1(func, ref(p), n);
	thread t2(func, ref(p), n);

	t1.join();
	t2.join();

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

在这个过程中两个线程会不断对引用计数进行自增和自减操作,理论上最终两个线程执行完毕后引用计数的值应该是1,因为拷贝出来的对象都被销毁了,只剩下最初的shared_ptr对象还在管理这个整型变量,但每次运行程序得到引用计数的值可能都是不一样的,根本原因就是因为对引用计数的自增和自减不是原子操作。

要解决引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数进行封装,这里以加锁为例:

  • 在shared_ptr类中新增互斥锁成员变量,为了让管理同一个资源的多个线程访问到的是同一个互斥锁,管理不同资源的线程访问到的是不同的互斥锁,因此互斥锁也需要在堆区创建。
  • 在调用拷贝构造函数和拷贝赋值函数时,除了需要将对应的资源和引用计数交给当前对象管理之外,还需要将对应的互斥锁也交给当前对象。
  • 当一个资源对应的引用计数减为0时,除了需要将对应的资源和引用计数进行释放,由于互斥锁也是在堆区创建的,因此还需要将对应的互斥锁进行释放。
  • 为了简化代码逻辑,可以将拷贝构造函数和拷贝赋值函数中引用计数的自增操作提取出来,封装成Add函数,将拷贝赋值函数和析构函数中引用计数的自减操作提取出来,封装成Release函数,这样就只需要对Add和Release函数进行加锁保护即可。

代码优化如下:

namespace gtt {
	template<class T>
	class shared_ptr
	{
	public:
		//构造函数
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _count(new int(1))
			,_mtx(new mutex)
		{}

		//析构函数
		~shared_ptr()
		{
			Release();
		}

		void Release()
		{
			_mtx->lock();
			bool flag = false;

			if (--(*_count) == 0) //count为0就需要释放资源
			{
				if (_ptr != nullptr)
				{
					cout << "delete[]:" << _ptr << endl;
					delete _ptr;
					_ptr = nullptr;
				}

				//释放引用计数资源
				delete _count;
				_count = nullptr;

				flag = true;
			}

			_mtx->unlock();

			if (flag)//锁资源最终也需要释放掉
			{
				delete _mtx;
				_mtx = nullptr;
			}
		}
		void AddCount()
		{
			_mtx->lock();

			(*_count)++;

			_mtx->unlock();
		}
		//拷贝构造函数
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _count(sp._count)
			,_mtx(sp._mtx)
		{
			AddCount();
		}

		//拷贝赋值函数
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();

				_ptr = sp._ptr;
				_count = sp._count;
				_mtx = sp._mtx;

				AddCount();
			}

			return *this;
		}

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

		T* operator->()
		{
			return _ptr;
		}
		//获取引用计数
		int use_count()
		{
			return *_count;
		}

		T* get()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _count;
		mutex* _mtx;

	};
}

注意

  • 在Release函数中,当引用计数被减为0时需要释放互斥锁资源,但不能在临界区中释放互斥锁,因为后面还需要进行解锁操作,因此代码中借助了一个flag变量,通过flag变量来判断解锁后释放需要释放互斥锁资源。

shared_ptr只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题,就像原生指针管理一块内存空间一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由这块空间的操作者来保证。

我们看下面这段代码:

struct Data
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void func(gtt::shared_ptr<Data>& sp, int n, mutex& mtx)
{
	for (int i = 0; i < n; i++)
	{
		shared_ptr<Data> copy(sp);

		sp->_year++;
		sp->_month++;
		sp->_day++;

	}
}
void test()
{
	gtt::shared_ptr<Data> p(new Data);
	cout << p.get() << endl;

	const size_t n = 50000;
	thread t1(func, ref(p), n);
	thread t2(func, ref(p), n);

	t1.join();
	t2.join();

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

	cout << p->_month << endl;
	cout << p->_year << endl;
	cout << p->_day << endl;
}

尽管我们在shared_ptr内部已经实现了引用计数安全,但是我们最终在调用Date成员变量进行++操作时,依旧不是线程安全的,因为我们是在shared_ptr外部实现++操作的,此时就需要我们自己进行加锁和解锁操作来实现线程安全。

struct Data
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void func(gtt::shared_ptr<Data>& sp, int n, mutex& mtx)
{
	for (int i = 0; i < n; i++)
	{
		shared_ptr<Data> copy(sp);

		mtx.lock();

		sp->_year++;
		sp->_month++;
		sp->_day++;

		mtx.unlock();
	}
}
void test()
{
	gtt::shared_ptr<Data> p(new Data);
	cout << p.get() << endl;

	mutex mtx;
	const size_t n = 50000;
	thread t1(func, ref(p), n, ref(mtx));
	thread t2(func, ref(p), n, ref(mtx));

	t1.join();
	t2.join();

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

	cout << p->_month << endl;
	cout << p->_year << endl;
	cout << p->_day << endl;

}

std::shared_ptr的循环引用

我们来看一段代码:

struct ListNode
{
	gtt::shared_ptr<ListNode> _prev;
	gtt::shared_ptr<ListNode> _next;

	int _data;

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

void test2()
{
	gtt::shared_ptr<ListNode> n1(new ListNode);
	gtt::shared_ptr<ListNode> n2(new ListNode);

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

上面程序正常情况下,调用结束以后n1, n2释放就会自动调用析构函数,释放空间,但是我们运行以后发现并没有,但是当我们注释掉n1->_next = n2n2->_prev = n1;中任意一句的时候,就会发现他就会调用析构函数来释放掉空间了,那么这是为什么呢?

如下图所示:

  1. 当以new的方式申请到两个ListNode结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1。
    C++之智能指针_第6张图片

  2. 如果我们此时只让n1->_next = n2或者是n2->_prev = n1,其中一个资源的引用计数就变为了2,我们以n1->_next = n2为例,此时资源2的引用计数就变为了2:
    C++之智能指针_第7张图片
    当函数调用结束以后,n1,n2的生命周期就到了,此时n2先释放,资源2引用计数--为1,释放完之后n1在释放,资源1引用计数--为0,因为n1->_next = n2,此时资源2引用计数在--为0,就完成了所有资源的释放。

  3. 如果我们将两个节点都链接起来,资源1当中的_next成员与n2一同管理资源2,资源2中的_prev成员与n1一同管理资源1,此时这两个资源对应的引用计数都被加到了2。

C++之智能指针_第8张图片
当出了main函数的作用域后,n1和2的生命周期也就结束了,因此这两个资源对应的引用计数最终都减到了1。如下图:
C++之智能指针_第9张图片
循环引用导致资源未被释放的原因:

  • 当资源对应的引用计数减为0时对应的资源才会被释放,因此资源1的释放取决于资源2当中的prev成员,而资源2的释放取决于资源1当中的next成员。
  • 而资源1当中的next成员的释放又取决于资源1,资源2当中的prev成员的释放又取决于资源2,于是这就变成了一个死循环,最终导致资源无法释放。

上述问题是如何解决的呢?此时就出现了我们的weak_ptr。

std::weak_ptr

weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。

  • weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数。

将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。

struct ListNode
{
	std::weak_ptr<ListNode> _prev;
	std::weak_ptr<ListNode> _next;

	int _data;

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

void test2()
{
	std::shared_ptr<ListNode> n1(new ListNode);
	std::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;
}

通过use_count获取这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数就是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数。

weak_ptr的模拟实现

简易版的weak_ptr的实现步骤如下:

  1. 提供一个无参的构造函数,比如刚才new ListNode时就会调用weak_ptr的无参的构造函数。
  2. 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr对象管理的资源。
  3. 支持用shared_ptr对象拷贝赋值给weak_ptr对象,赋值时获取shared_ptr对象管理的资源。
  4. 对*和->运算符进行重载,使weak_ptr对象具有指针一样的行为。
namespace
{
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

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

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

std::shared_ptr的定制删除器

当智能指针对象的生命周期结束时,所有的智能指针默认都是以delete的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以new方式申请到的内存空间,智能指针管理的也可能是以new[]的方式申请到的空间,或管理的是一个文件指针。

struct ListNode
{
	ListNode* _prev;
	ListNode* _next;

	int _data;

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

void test2()
{
	gtt::shared_ptr<ListNode> sp1(new ListNode[10]);
	gtt::shared_ptr<FILE> sp2(fopen("test.cpp", "r"));
}

这时当智能指针对象的生命周期结束时,再以delete的方式释放管理的资源就会导致程序崩溃,因为以new[]的方式申请到的内存空间必须以delete[]的方式进行释放,而文件指针必须通过调用fclose函数进行释放。

此时,我们就需要定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数:

template <class U, class D>
shared_ptr (U* p, D del);

参数说明

  • p:需要让智能指针管理的资源。
  • del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。

因此当智能指针管理的资源不是以new的方式申请到的内存空间时,就需要在构造智能指针对象时传入定制的删除器。

template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout << "delete[] ptr" << endl;
		delete[] ptr;
	}
};

void test2()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10], DeleteArray<ListNode>());
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr)
		{
			cout << "lambda fclose" << ptr << endl;
			fclose(ptr);
		});
	std::shared_ptr<Date> sp3(new Date[10], [](Date* ptr)
		{
			cout << "lambda delete" << ptr << endl;
			delete[] ptr; 
		});
}

定制删除器的实现问题:

  • C++标准库中实现shared_ptr时是分成了很多个类的,因此C++标准库中可以将删除器的类型设置为构造函数的模板参数,然后将删除器的类型在各个类之间进行传递。
  • 但我们是直接用一个类来模拟实现shared_ptr的,因此不能将删除器的类型设置为构造函数的模板参数。因为删除器不是在构造函数中调用的,而是需要在Release函数中进行调用,因此势必需要用一个成员变量将删除器保存下来,而在定义这个成员变量时就需要指定删除器的类型,因此这里模拟实现的时候不能将删除器的类型设置为构造函数的模板参数。
  • 要在当前模拟实现的shared_ptr的基础上支持定制删除器,就只能给shared_ptr类再增加一个模板参数,在构造shared_ptr对象时就需要指定删除器的类型。然后增加一个支持传入删除器的构造函数,在构造对象时将删除器保存下来,在需要释放资源的时候调用该删除器进行释放即可。最好在设置一个默认的删除器,如果用户定义shared_ptr对象时不传入删除器,则默认以delete的方式释放资源。
namespace gtt {
	template<class T>
	class shared_ptr
	{
	public:
		//构造函数
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _count(new int(1))
			,_mtx(new mutex)
		{}

		template<class D>
		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _count(new int(1))
			, _mtx(new mutex)
			,_del(del)
		{}

		//析构函数
		~shared_ptr()
		{
			Release();
		}

		void Release()
		{
			_mtx->lock();
			bool flag = false;

			if (--(*_count) == 0) //count为0就需要释放资源
			{
				if (_ptr != nullptr)
				{
					//cout << "delete[]:" << _ptr << endl;
					//delete _ptr;
					//_ptr = nullptr;
					//删除器删除
					_del(_ptr);
				}

				//释放引用计数资源
				delete _count;
				_count = nullptr;

				flag = true;
			}

			_mtx->unlock();

			if (flag)//锁资源最终也需要释放掉
			{
				delete _mtx;
				_mtx = nullptr;
			}
		}
		void AddCount()
		{
			_mtx->lock();

			(*_count)++;

			_mtx->unlock();
		}
		//拷贝构造函数
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _count(sp._count)
			,_mtx(sp._mtx)
		{
			AddCount();
		}

		//拷贝赋值函数
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();

				_ptr = sp._ptr;
				_count = sp._count;
				_mtx = sp._mtx;

				AddCount();
			}

			return *this;
		}

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

		T* operator->()
		{
			return _ptr;
		}
		//获取引用计数
		int use_count()
		{
			return *_count;
		}

		T* get()const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _count;
		mutex* _mtx;
		
		//设置为包装器类型,可以接收任意类型
		function<void(T*)> _del = [](T* ptr) {
			cout << "lambda delete:" << ptr << endl;
			delete ptr;
		};
	};
}

C++11和boost中智能指针的关系

  • C++98中产生了第一个智能指针auto_ptr。
  • C++boost给出了更实用的scoped_ptr、shared_ptr和weak_ptr。
  • C++TR1,引入了boost中的shared_ptr等。不过注意的是TR1并不是标准版。
  • C++11,引入了boost中的unique_ptr、shared_ptr和weak_ptr。需要注意的是,unique_ptr对应的就是boost中的scoped_ptr,并且这些智能指针的实现原理是参考boost中实现的。

你可能感兴趣的:(c++)