详解c++---智能指针

目录标题

为什么会有智能指针

根据前面的知识我们知道使用异常可能会导致部分资源没有被正常释放,因为异常抛出之后会直接跳转到捕获异常的地从而跳过了一些很重要的的代码,比如说下面的情况:

cpp 复制代码
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;
	cout << "delete p1" << endl;
	delete p1;
	cout << "delete p2" << endl;
	delete p2;
}
int main()
{
	try{Func();}
	catch (exception& e)
	{cout << e.what() << endl;}
	return 0;
}

main函数中调用了func函数,func函数里面调用了div函数,func函数中没有捕捉异常,但是在main函数里面却捕捉了异常,所以出现异常的话就会导致func函数中的部分代码没有被执行,进而导致内存泄漏,比如说下面的运行结果,没有除0那么运行的结果就是正确的:

如果除以0的话p1和p2指向的空间就无法被正常的释放:

那么为了解决这个问题我们就有了重新抛出这个概念,在func函数里面添加捕获异常的代码,然后在catch里面对资源进行释放最后重新将异常进行抛出,最后交给main函数中的catch进行处理,比如说下面的代码:

cpp 复制代码
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;
	try
	{
		cout << div() << endl;
	}
	catch (exception& e)
	{
		cout << "delete p1" << endl;
		delete p1;
		cout << "delete p2" << endl;
		delete p2;
		throw invalid_argument("除0错误");
	}
}
int main()
{
	try{Func();}
	catch (exception& e)
	{cout << e.what() << endl;}
	return 0;
}

这样在面都除0问题时就不会忘记释放资源,比如说下面的运行结果:

可以看到这里即使除以了0依然可以正常地释放上面地资源,但是这么写难道就完成正确了吗?肯定没有,因为new本身也是会抛异常的,当内存不足却又使用new申请空间的话就会导致开辟空间失败从而抛出异常,那new抛异常会导致什么结果呢?我们来分情况进行讨论,首先p1抛出异常会有什么问题吗?答案是没有p1抛出异常会直接跳转到main函数里面进行捕捉并且p2还没有开辟p1没有开辟成功,从而不会导致任何的内存泄漏,那要是p2开辟失败了呢?这时候也是会直接跳转到main函数里面进行捕捉,但是p1已经开辟空间了啊,所以如果p2开辟空间失败他会导致p1的申请的资源没有被正常释放,所以为了安全起见我们给p2也添加一个try上去 并且try块里面还得含有后面的代码,因为一旦内存申请失败后面的调用函数也无需执行了,那么这里的代码如下:

cpp 复制代码
void Func()
{
	int* p1 = new int;
	try 
	{
		int* p2 = new int;
		try{cout << div() << endl;}
		catch (exception& e)
		{
			cout << "delete p1" << endl;
			delete p1;
			cout << "delete p2" << endl;
			delete p2;
			throw invalid_argument("除0错误");
		}
	}
	catch (...)
	{
		//...
	}
}

并且里面的div也会抛出异常,这个异常可能会被func函数的最外层的catch进行捕捉,那这在处理异常的时候是不是就很麻烦啊!并且我们这里只申请了两个整型变量,那如果有三个有四个甚至更多的话又该如何来嵌套try catch呢?所以当出现连续抛出异常的情况时,我们之前学习的try catch语句就很难进行应对,那么为了解决这个问题有人就提出了智能指针这个东西。

智能指针模拟实现

首先智能指针是一个类,并且这个类要处理各种各样的数据,所以这个类就得是模板类,比如说下面的代码:

cpp 复制代码
template<class T> 
class Smart_Ptr
{
public:
private:
	T* _ptr;
};

然后还需要构造函数和析构函数,构造函数就需要一个参数,然后拿这个参数来初始化内部的_ptr就行,还有一个就是析构函数,析构函数就是在内部使用delete释放指针指向的空间即可,那么这里的代码如下:

cpp 复制代码
template<class T> 
class Smart_Ptr
{
public:
	Smart_Ptr(T*ptr)
		:_ptr(ptr)
	{}
	~Smart_Ptr()
	{
		delete[] _ptr;
		cout << "~ptr:" <<_ptr <<endl;
	}
private:
	T* _ptr;
};

有了这个智能指针类之后我们就可以将申请资源的地址赋值给智能指针对象,从而解决上述的问题,比如说下面的代码:

cpp 复制代码
void Func()
{
	int* p1 = new int;
	int* p2 = new int;
	Smart_Ptr<int> sp1 = p1;
	Smart_Ptr<int> sp2 = p2;
	cout << div() << endl;
	cout << "delete p1" << endl;
	delete p1;
	cout << "delete p2" << endl;
	delete p2;
	throw invalid_argument("除0错误");
}

代码的运行结果如下:

可以看到即使出现了除0错误这里也可以将两个申请的空间进行释放,原理就是智能指针对象的生命周期属于Func函数,当除0错误抛出异常的时候会直接跳转到main函数里面,这个时候Func函数也跟着结束,它一结束类对象的生命也就跟着结束,结束的时候就会调用析构函数来释放空间,所以就解决了之前的问题,那么要是new的时候抛出异常这里也能解决吗?答案是可以的原理也一摸一样这里就不多描述。当然只有构造函数和析构函数还完全达不到我们的需求,构造函数负责存储资源,析构函数负责释放资源,那么我们还需要一些函数来帮助我们使用这些资源,于是我们就可以添加三个操作符的重载函数,一个是解引用重载,一个是->操作符重载,一个是方括号重载,那么这三个函数的实现就如下:

cpp 复制代码
template<class T> 
class Smart_Ptr
{
public:
	Smart_Ptr(T*ptr)
		:_ptr(ptr)
	{}
	T& operator *(){return *_ptr;}
	T* operator->(){return _ptr;}
	T& operator[](size_t pos) { return _ptr[pos]; }
	
	~Smart_Ptr()
	{
		delete[] _ptr;
		cout << "~ptr:" <<_ptr <<endl;
	}
private:
	T* _ptr;
};

有了这三个函数之后我们就可以通过智能指针来修改地址指向的内容,比如说下面的代码:

cpp 复制代码
int main()
{
	Smart_Ptr<int> sp1(new int[10]{ 1,2,3,4,5 });
	cout << sp1[3] << endl;
	sp1[3]++;
	cout << sp1[3] << endl;
	return 0;
}

代码的运行结果如下:

可以看到这里实现了内部数据读取的功能,我们把上面智能指针的形式称为RAII,RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在

对象析构的时候释放资源,借此,我们实际上把管理一份资源的责任托管给了一个对象,也就是说把资源和对象绑定在一起,对象什么时候结束资源就什么时候释放。这种做法有两大好处:不需要显式地释放资源。采用这种方式,对象所需的资源在其生命期内始终保持有效。

库中的智能指针

在看库中的智能指针之前我们首先来看看下面这段代码:

cpp 复制代码
void func2()
{
	Smart_Ptr<int>sp1(new int(10));
	Smart_Ptr<int>sp2(sp1);
}

将这段代码运行一下便会发现我们实现的智能指针类存在问题:

原因很简单我们自己实现的类里面没有拷贝构造函数,所以编译器自动生成了一个,并且采用浅拷贝的方式进行构造,这样就导致两个智能指针指向了同一个空间,所以当函数调用结束,对象的生命周期结束时就会调用delete将同一份空间析构两次,所以就会报出上面的错误,那么为了解决这个问题我们就得自己来实现一个拷贝构造函数,可是这里的拷贝构造,并不能是深拷贝因为我们这个类的目的就是让其想指针一样,当我们把一个指针赋值给另外一个指针时,是两个指针指向同一块空间,而不是两个指针分别指向两个空间,所以这里就不能采用深拷贝的形式来进行拷贝构造,那么库中是如何来解决这个问题的呢?我们首先来看看出现最早的auto_ptr来如何解决这个问题。

auto_ptr

我们首先来看看auto_ptr的介绍:

使用方法跟我们自己实现的智能指针是一样的,那么我们就可以写出下面这样的代码:

cpp 复制代码
void func2()
{
	auto_ptr<int>sp1(new int(10));
	auto_ptr<int>sp2(sp1);
}

将代码运行一下便可以看到这里的结果是没有问题的:

但是我们再使用解引用查看指针指向的内容时就会发现这里报错了比如说下面的代码:

cpp 复制代码
void func2()
{
	auto_ptr<int>sp1(new int(10));
	auto_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

代码的运行结果如下:

那这报错的原因就和auto_ptr实现的方法有关,auto_ptr的解决方法就是将管理权进行转移,把原来的智能指针变为空,让新的智能指针指向这个空间,比如说下面的图片:

拷贝构造之前sp1里面储存着数据,但是经历完拷贝构造之后sp1里面的数据就变成下面这样:

所以上面报错的原因就是对空指针进行了解引用访问,那么如果我们想要实现上面这种形式的拷贝构造的话就得将参数中的const去掉,比如说下面的代码:

cpp 复制代码
Smart_Ptr(Smart_Ptr<T>& sp)
	:_ptr(sp._ptr)
{
	sp._ptr = nullptr;
}

那么这就是auto_ptr的实现原理大家大可不必了解因为这种形式没人用。甚至很多公司都明确要求不能使用这种形式的智能指针,原因就是auto_ptr使用的方式太不合常理了,那么后来为了解决auto_ptr难用的问题,就有了unique_ptr和share_ptr/weak_ptr。

unique_ptr

首先来看看这个函数的介绍:

这个智能指针支持的操作:

那么我们来看看使用unique_ptr会不会也出现auto_ptr的问题,那么这里的代码如下:

cpp 复制代码
#include<memory>
void func2()
{
	unique_ptr<int>sp1(new int(10));
	unique_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

将代码运行一下便可以看到下面这样的结果:

可以看到使用unique_ptr也会出现问题但是这个问题跟auto_ptr不一样,它是因为拷贝构造函数被删除了所以出现了问题,那么通过这个例子我们便可以知道unique_ptr解决拷贝构造问题的思路就是直接不让用拷贝构造,那么这里的实现逻辑就如下:

cpp 复制代码
Smart_Ptr(const Smart_Ptr<T>& sp) = delete;

这种实现逻辑肯定不大行,所以我们也没有必要过深的了解,我们接着看下一个形式的智能指针。

shared_ptr

首先来看看这个智能指针的介绍:


看起来跟上面的使用方法差不多,那么我们就来测试一下这个形式的智能指针会不会出现之前的问题,那么这里的代码如下:

cpp 复制代码
void func2()
{
	shared_ptr<int>sp1(new int(10));
	shared_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
	*sp1=20;
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

代码的运行结果如下:

可以看到使用share_ptr既不会出现不让拷贝的情况,也不会出现拷贝完置空的情况,而且这个智能指针跟普通的指针一样指向的是同一块区域的内容,那么这就是符合我们需求的智能指针,那么它是如何实现的呢?原理很简单采用引用计数的方式来实现,当使用智能指针存储数据时我们还开辟一个空间用整型来存储当且这个数据被多少个对象所指向,当被指向的对象数目为0时析构函数里面就使用delete来释放这个空间,比如说下面的图片:

一个智能指针指向两块空间,一块数据用于存储数据另一块空间就记录当前空间被几个智能指针所指向,当前只有对象sp1指向这个空间所以当前的计数就为1,当我们再创建一个对象sp2并指向这个空间时图片就变成了下面这样:

因为多了一个对象指向所以计数变量变成了2,当sp1对象生命周期结束,或者sp1指向其他内容时,图片就变成了下面这样:

因为指向的对象变少所以技术变量由2变成了1那么这就是share_ptr的存储原理,但是这里就有个问题计数变量的空间如何来分配呢?可以是个普通的整型变量放到对象里面吗?很明显不可以因为当计数变量的值发生改变时,所有指向该空间对象的内部计数变量都得改变,那我怎么找到这些对象啊对吧,敌人在暗我在明,很明显这是很难实现的,那么可以使用静态成员变量来实现吗?感觉上好像可以,因为不管类实例化出来了多少个对象,这个静态变量只有一个,并且所有对象都会共享这个静态变量,那么这时只要一个对象对这个静态变量进行修改的话,其他对象都会跟着一起修改,那这是不是就达到了我们的目的呢?其实没有,因为静态变量虽然一个对象修改所有对象都可以查看,但是这个对象指的是这个类所有实例化出来的对象,有些智能指针指向的是整型数组arr1,有些智能指针指向的是整型数组arr2,但是因为静态变量只有一个所以他们内部的计数变量都是一样的值,那这是不是就不符合逻辑了啊对吧,所以使用静态变量来计数的方式也是不行的,那么这里就只剩下最后一个方法在构造函数里面使用new开辟一个空间,空间里面记录被指向的个数,然后在类里面添加一个整型的指针变量,让指针指向开辟的空间,那么这里的构造函数代码就如下:

cpp 复制代码
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}
private:
	int* _pcount;
	T* _ptr;
};

拷贝构造就只需要赋值两个指针的值,然后对整型指针指向的内容加一就可以,比如说下面的代码:

cpp 复制代码
shared_ptr(const shared_ptr<T>& sp)
	:_ptr(sp._ptr)
	, _pcount(sp._pcount)
{
	++(*_pcount);
}

析构函数在释放空间的时候就得先进行一下判断,如果当前对象的计数变量等于1的话我们就将两个空间全部都释放掉,如果计数的变量大于1的话我们就将计数的值减去1就可以了,那么这里的代码如下:

cpp 复制代码
~shared_ptr()
{
	if (--(*_pcount) == 0)
	{
		delete _pcount;
		delete _ptr;
	}
}

剩下的三个操作符重载也是同样的道理这里就不多解释,直接上代码:

cpp 复制代码
T& operator*()
{
	return *_ptr;
}
T* operator->()
{
	return  _ptr;
}
T& operator[](size_t pos)
{
	return _ptr[pos];
}

那么接下来我们就可以使用下面的代码来进行一下测试:

cpp 复制代码
void func2()
{
	YCF::shared_ptr<int>sp1(new int(10));
	YCF::shared_ptr<int>sp2(sp1);
	cout << *sp1 << endl;
	cout << *sp2 << endl;
	*sp1=20;
	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

代码运行的结果如下:

可以看到结果是正常的,并且通过调试可以看到一开始sp1的计数变量一开始为1:

当我们拷贝构造完之后就可以看到这两个对象的计数变量全部都变成了2:

内部指针指向的地址也都是一样的,那么这就说明我们实现的方法是正确的,那么接下来我们就来看看如何实现赋值重载,首先赋值重载不允许自己给自己重载,所以先使用if语句判断一下传递过来的对象和该对象内部的_ptr是否相同,相同的话我们就不做任何的操作,如果不相同的话就执行后面的操作:

cpp 复制代码
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	if (_ptr != sp._ptr)
	{

	}
}

如果指针不相同我们就先判断一下当前类的计数变量是否为1,如果为1的话我们就释放该对象的两个空间,并指向参数中的两个空间,并对参数的计数变量进行加加,如果当前的计数变量大于1的话我们就对本身的计数变量进行减减,然后改变两个指针的指向即可,那么这里的代码就如下:

cpp 复制代码
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	if (_ptr != sp._ptr)
	{
		if (--(*_pcount) == 0)
		{
			delete _pcount;
			delete _ptr;
		}	
		_pcount = sp._pcount;
		_ptr = sp._ptr;
		++(*_pcount);
	}
}

那么我们就可以使用下面的代码来进行测试:

cpp 复制代码
void func2()
{
	YCF::shared_ptr<int>sp1(new int(10));
	YCF::shared_ptr<int>sp2(new int(20));
	YCF::shared_ptr<int>sp3(sp1);
	YCF::shared_ptr<int>sp4(sp2);
	cout << "*sp1: " << *sp1 << endl;
	cout << "*sp2: " << *sp2 << endl;
	cout << "*sp3: " << *sp3 << endl;
	cout << "*sp4: " << *sp4 << endl;
	sp1 = sp2;
	cout << "*sp1: " << *sp1 << endl;
	cout << "*sp2: " << *sp2 << endl;
	cout << "*sp3: " << *sp3 << endl;
	cout << "*sp4: " << *sp4 << endl;
}

代码的运行结果如下:

可以看到这里的运行结果符合我们的需求,通过调试可以看到在没有赋值之前sp1和sp3指向的地址是一样的并且引用计数为2,sp2和sp4指向的空间是一样并且引用计数的值也为2

当我们把sp2赋值给sp1时就可以看到sp1 sp2 sp4指向的地址时一样的并且引用计数变成了3,而sp3的引用计数则变成了1:

那么这就是shared_ptr的模拟实现完整的代码如下:

cpp 复制代码
namespace YCF
{
	template<class T>
	class shared_ptr
	{
		public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				if (--(*_pcount) == 0)
				{
					delete _pcount;
					delete _ptr;
				}	
				_pcount = sp._pcount;
				_ptr = sp._ptr;
				++(*_pcount);
			}
			return *this;
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);
		}
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				delete _pcount;
				delete _ptr;
			}
		}
		T& operator*(){return *_ptr;}
		T* operator->(){return _ptr;}
		T& operator[](size_t pos){return _ptr[pos];}
	private:
		int* _pcount;
		T* _ptr;
	};
}

智能指针的线程安全问题

在我们写代码的时候可能会出现多个线程共享同一个资源的情况,那我们上面实现的智能指针在面对多线程的时候会不会出现问题呢?我们添加一个函数来进行验证

cpp 复制代码
int get()
{
	return *_pcount;
}

这个函数就可以帮助我们获取对象里面的计数变量的值,然后我们就可以用下面的代码来进行测试:

cpp 复制代码
void func3()
{
	int n = 50000;
	YCF::shared_ptr<int> sp1(new int(10));
	thread t1([&]()
		{
			for (int i = 0; i < n; i++)
			{
				YCF::shared_ptr<int> sp2(sp1);
			}
		});
	thread t2([&]()
		{
			for (int i = 0; i < n; i++)
			{
				YCF::shared_ptr<int> sp3(sp1);
			}
		});
	t1.join();
	t2.join();
	cout << sp1.get() << endl;
}

如果运行的结果一直为1就说明代码是安全的,如果运行的结果出现了其他的值就说明上面的代码存在问题:


可以看到这里运行了多次但是每次的结果都不大一样,那这是为什么呢?原因很简单,多个线程在工作的时候都是相互独立,进程1对计数变量++的时候进程2也会拿这个变量进行++,但是++并不是一步就能完成的它也需要一些步骤,这就会导致一个进程还没有对这个变量执行完加一另外一个进程就会接着拿这个变量执行加一,比如说当前的计数变量x的值为1,进程1拿x进行++但是++分为3步,当进程1执行到第一步的时候进程2又会拿x的值进行++,进程1执行完的结果是2,进程2执行完的结果也是2,所以这就导致了我们创建了两个智能指针对象,但是这个空间对应的计数变量只增加了1,但是析构的时候可能又不会出现问题正常的对计数变量减二,那么这就是问题所在,我们上面执行的结果大于1就是因为析构的时候出现了问题但是构造的时候却没有问题,所以就大于1,拿如何来解决这个问题呢?答案是添加锁上去

mutex对象是不让使用者进行拷贝:

并且指向同一块空间的对象得用同一把锁,才能保证空间的使用不发生冲突,所以在创建智能指针对象的时候得再申请一块空间用于存放锁,所以我们还得往类对象里面添加一个锁类型的指针,在构造函数里面讲锁指针指向新创建出来的锁,拷贝构造的时候将锁指针进行拷贝,那么这里的代码就如下:

cpp 复制代码
template<class T>
class shared_ptr
{
	public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		,_pmtx(new mutex)
	{}
private:
	int* _pcount;
	T* _ptr;
	mutex* _pmtx;
};

然后在拷贝构造的时候就得在++前面添加锁,加加执行完成之后再解锁,有了锁之后当进程1执行这个代码进程2就只能在外面等着,等进程1解锁以后再执行锁里面的代码,并对代码进行上锁,那么拷贝构造的代码就如下:

cpp 复制代码
shared_ptr(const shared_ptr<T>& sp)
	:_ptr(sp._ptr)
	, _pcount(sp._pcount)
	,_pmtx(sp._pmtx)
{
	_pmtx->lock();
	++(*_pcount);
	_pmtx->unlock();
}

对于赋值重载也是一样的道理,在++之前对代码进行上锁,在++之后对代码进行解锁,那么这里的代码如下:

cpp 复制代码
void Release()
{
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
		delete _pcount;
		delete _ptr;
	}
	_pmtx->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	if (_ptr != sp._ptr)
	{
		Release();
		_pcount = sp._pcount;
		_ptr = sp._ptr;
		_pmtx = sp._pmtx;
		_pmtx->lock();
		++(*_pcount);
		_pmtx->unlock();
	}
	return *this;
}

那么有了锁之后我们在运行上面的代码就可以看到不管程序运行多少次,执行的结果都是1:

虽然添加锁可以帮助我们避免出错,但是在析构函数的我们还得对这个锁进行删除,那我们能可以这么写吗?

cpp 复制代码
void Release()
{
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
		delete _pcount;
		delete _ptr;
		delete _pmtx;
	}
	_pmtx->unlock();
}
~shared_ptr()
{
	Release();
}

将代码运行一下便可以看到这样的错误:

原因也很简单因为删除锁的时候锁还处于工作状态,所以我们就创建一个变量flag用来记录当前是否能够删除锁,如果进入了if语句里面我们就修改flag的值,锁解开之后就判断flag的值如果flag为true的话我们就删除锁,那么这里的代码就如下:

cpp 复制代码
void Release()
{
	bool flag = false;
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
		delete _pcount;
		delete _ptr;
		flag = true;
	}
	_pmtx->unlock();
	if (flag)
	{
		delete _pmtx;
	}
}
~shared_ptr()
{
	Release();
}

这里的flag不会有线程安全问题,因为多线程的局部变量存储的位置在栈上,而每个线程都有一个属于自己的栈,所以不会发生冲突,计数变量有问题是因为这个变量存储的位置在堆上,不管我们有多少个线程堆都只有一个,所以就可能出现问题,通过上面的改动我们让智能指针的内部计数变量是线程安全的,但是智能指针指向的对象一定是线程安全的吗?我们来看看下面的代码:

cpp 复制代码
struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
void func3()
{
	int n = 50000;
	YCF::shared_ptr<Date> sp1(new Date);
	thread t1([&]()
		{
			for (int i = 0; i < n; i++)
			{
				YCF::shared_ptr<Date> sp2(sp1);
				sp2->_day++;
				sp2->_month++;
				sp2->_year++;
			}
		});
	thread t2([&]()
		{
			for (int i = 0; i < n; i++)
			{
				YCF::shared_ptr<Date> sp3(sp1);
				sp3->_day++;
				sp3->_month++;
				sp3->_year++;
			}
		});
	t1.join();
	t2.join();
	cout << "day: " << sp1->_day << endl;
	cout << "month: " << sp1->_month << endl;
	cout << "year: " << sp1->_year << endl;
}

代码的运行结果如下:

可以看到上面改进得到的结果是让智能指针的计数变量变得安全,但是没有让智能指针指向的对象变得安全,原理也很简单,我们上面做的改进都是在对象的内部,而对智能指针指向的对象都是在对象的外部,所以这两个是没有关系的,那么要想保证指向的内容也是线程安全的话就得在修改的时候也添加锁上去,那么这里的代码就如下:

cpp 复制代码
void func3()
{
	int n = 50000;
	mutex mtx;//这个也可以被捕捉
	YCF::shared_ptr<Date> sp1(new Date);
	thread t1([&]()
		{
			for (int i = 0; i < n; i++)
			{
				YCF::shared_ptr<Date> sp2(sp1);
				mtx.lock();
				sp2->_day++;
				sp2->_month++;
				sp2->_year++;
				mtx.unlock();
			}
		});
	thread t2([&]()
		{
			for (int i = 0; i < n; i++)
			{
				YCF::shared_ptr<Date> sp3(sp1);
				mtx.lock();
				sp3->_day++;
				sp3->_month++;
				sp3->_year++;
				mtx.unlock();
			}
		});
	t1.join();
	t2.join();
	cout << "day: " << sp1->_day << endl;
	cout << "month: " << sp1->_month << endl;
	cout << "year: " << sp1->_year << endl;
}

代码的运行结果如下:

那么这时智能指针指向的内容和智能指针的内部计数变量都是线程安全的了。

循环智能指针

首先我们创建一个名为listnode的类,类里面含有两个listnode的指针和一个int的变量用来存储数据,然后创建一个析构函数用来作为标记,那么这里的代码就如下:

cpp 复制代码
struct List_Node
{
	List_Node* prev;
	List_Node* next;
	int val;
	~List_Node()
	{
		cout << "~List_Node" << endl;
	}
};

然后我们就可以使用下面的代码来进行测试:

可以看到运行的结果是正常的,那如果我们使用shared_ptr来存储这里的指针的话还能够正常的编译吗?我们来看看下面的代码:

cpp 复制代码
void func4()
{
	YCF::shared_ptr<List_Node> n1 = new List_Node;
	YCF::shared_ptr<List_Node> n2 = new List_Node;
	n1->next = n2;
	n2->prev = n1;
}

但是这么修改存在一个问题,因为类里面指针的类型为List_Node*,而n2和n1都是智能指针类型,智能指针怎么可以赋值给普通指针呢?对吧,所以就报错了,那么解决问题的方法也很简单,将List_Node里面的普通指针修改成为智能指针就可以了,那么这里的代码如下:

cpp 复制代码
struct List_Node
{
	YCF::shared_ptr<List_Node> prev;
	YCF::shared_ptr<List_Node> next;
	int val;
	~List_Node()
	{
		cout << "~List_Node" << endl;
	}
};

将代码运行一下便会出现下面这样的结果:

上面的代码没有调用析构函数,那这是为什么呢?我们可以画图来进行一下分析,首先每个List_Node长成下面这样:

我们上面的代码创建了两个list_node对象,那么就变成下面这样:

并且我们还创建两个共享指针指向分别指向这两个对象,那么这时图片就变成下面这样:

然后我们又干了一件事就是让两个list_node里面的一个shared_ptr相互指向,那么这时图片就变成下面这个样子:

内部计数变量全部都变成了2,然后函数的调用就结束了,我们在函数里面创建了两个shared_ptr所以随着函数的结束,这两个指针也就跟着销毁了,每当一个共享指针停止了指向该空间,那么指向这个空间的其他共享指针的引用计数就要减一,所以上面的图片就会变成下面这样:

我们说当智能指针内部的引用计数变为0时,指针指向的对象就会被释放,也就是说上图左边的对象要想释放的话就得先释放右边的对象(因为对象被销毁了里面的智能指针就会才会跟着被销毁),而要想释放右边的对象就得先释放左边的对象所以这里就产生了矛盾,所以上面的代码到最后没有显示调用析构函数,那么根据shared_ptr的实现逻辑是无法解决上述的问题的,所以c++又引入了一个新的智能指针叫weak_ptr,它可以指向资源访问资源但是它不能管理资源:

我们可以把shared_ptr看成大哥,那么weak_ptr就是它的小弟,weak_ptr不支持RAII也就是不支持管理资源,但是它支持shared_ptr的拷贝,也就是用shared_ptr构造weak_ptr是可以的,那么这里我们就可以用下面的代码来进行一下测试,如果list_node的内部是shared_ptr实现的话下面代码的运行结果会是如何:

cpp 复制代码
void func4()
{
	YCF::shared_ptr<List_Node> n1 = new List_Node;
	YCF::shared_ptr<List_Node> n2 = new List_Node;
	n1->next = n2;
	n2->prev = n1;
	cout << n1.get() << endl;
	cout << n2.get() << endl;
}

很明显都是2

但如果将list_node的内部修改成为weak_ptr的话,又会是如何呢?比如说下面的代码:

cpp 复制代码
struct List_Node
{
	std::weak_ptr<List_Node> prev;
	std::weak_ptr<List_Node> next;
	int val;
	~List_Node()
	{
		cout << "~List_Node" << endl;
	}
};
void func4()
{
	std::shared_ptr<List_Node> n1 (new List_Node);
	std::shared_ptr<List_Node> n2 (new List_Node);
	n1->next = n2;
	n2->prev = n1;
	cout << n1.use_count() << endl;//得到引用计数的值
	cout << n2.use_count() << endl;
}

代码的运行结果如下:

可以看到这里的引用计数就变成了1,那么这就是因为weak_ptr只负责指向不会负责管理,weak_ptr指向一块空间,并不会引起该空间其他的共享指针的引用计数的值,那么这就是它的功能,当我们发现一些场景可能会出现循环引用的现象时我们就该使用weak_ptr来指向而不是shared_ptr,那weak_ptr是如何实现的呢?我们接着往下看。

weak_ptr

这里我们就实现一个大概的逻辑,库中的智能指针会比我们实现的更加复杂一些,那么我们这里的实现就只是为了让大家能够更好的理解这里的逻辑,那么这个容器的实现就很简单,他內部就只有一个指针变量用来指向空间,然然后无参的构造函数就是将指针初始化为空就可以了,那么这里的代码就如下:

cpp 复制代码
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
	public:
		T* _ptr;
	};
}

然后就是拷贝构造函数,他就是将传递过来shared_ptr里面的指针赋值过来就可以了,当然这里可能会存在一个问题可能传递过来的shared_ptr内部的指向的空间已经消失了,所以库中的实现这里的时候还做了判断,但是我们这里只是大致的实现所以就不考虑这么多,那么这里的代码就如下:

cpp 复制代码
weak_ptr(const shared_ptr<T>& sp)
	:_ptr(sp.get())
	//这里改了一下get是获取内容的地址
{}

那么赋值重载也是同样的道理:

cpp 复制代码
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	_ptr = sp.get();
	return *this;
}

最后添加几个操作符重载函数:

cpp 复制代码
// 像指针一样
T& operator*()
{
	return *_ptr;
}
T* operator->()
{
	return _ptr;
}

这样我们的weak_ptr就实现完成了,我们再用一下自己的实现的weak_ptr来进行一下测试:

cpp 复制代码
struct List_Node
{
	YCF::weak_ptr<List_Node> prev;
	YCF::weak_ptr<List_Node> next;
	int val;
	~List_Node()
	{
		cout << "~List_Node" << endl;
	}
};
void func4()
{
	YCF::shared_ptr<List_Node> n1 (new List_Node);
	YCF::shared_ptr<List_Node> n2 (new List_Node);
	n1->next = n2;
	n2->prev = n1;
	cout << n1.use_count() << endl;//得到引用计数的值
	cout << n2.use_count() << endl;
}

运行的结果如下:

可以看到符合我们的预期,那么weak_ptr的模拟实现就完成了。

定制删除器

我们的shared_ptr的析构函数实现方法如下:

cpp 复制代码
void Release()
{
	bool flag = false;
	_pmtx->lock();
	if (--(*_pcount) == 0)
	{
		delete _pcount;
		delete _ptr;
		flag = true;
	}
	_pmtx->unlock();
	if (flag)
	{
		delete _pmtx;
	}
}
~shared_ptr()
{
	Release();
}

我们说使用new申请一个类型空间的时候是用delete来释放空间,而当我们使用new来申请一个数组的空间时则是用delete [ ]来释放空间,但是我们怎么知道使用者是拿共享指针指向单个数据还是一个数组呢?比如说下面的代码:

cpp 复制代码
void func5()
{
	YCF::shared_ptr<string> n1(new string[10]);
}

代码的运行结果如下:

可以看到这里直接报错了,但是这里的报错并不是我们的问题,使用库中的共享指针也会出现相同的问题,比如说下面的代码:

cpp 复制代码
void func5()
{
	std::shared_ptr<string> n1(new string[10]);
}

代码的运行结果如下:

那么为了解决这个问题我们就提出一个概念叫做定制删除器,通过观察库中的文档便可以看到定制删除器的身影:

那么这个定制删除器就相当于是一个仿函数,我们自己实现的删除方式在删除一些数据的时候可能会出现问题,那么这个时候你就可以给我们提供一个删除方式,如果你提供了我们就用你提供的方式来进行删除,比如说上面在删除数组的时候出现了问题,我们就可以提供一个放函数专门用来释放数组的数据,那么这里的代码就如下:

cpp 复制代码
template<class T>
struct DeleteArray
{
	void operator()(const T* ptr)
	{
		delete[] ptr;
		cout << "delete [] "<<ptr<< endl;
	}
};

那么我们将这个定制删除器传递过去比如说下面的代码:

cpp 复制代码
void func5()
{
	std::shared_ptr<string> n1(new string[10],DeleteArray<string>());
}

再运行一下便可以发现是没有错的:

并且这里不仅可以传递仿函数还可以传递lambda表达式,比如说下面的代码:

cpp 复制代码
void func5()
{
	std::shared_ptr<string> n1(new string[10], DeleteArray<string>());
	std::shared_ptr<string> n2(new string[10], [](string* ptr) {delete[] ptr; });
}

并且我们还可以使用share_ptr打开文件然后在传递定制删除器的时候使用lambda加fclose来关闭文件,比如说下面的代码:

cpp 复制代码
void func5()
{
	std::shared_ptr<string> n1(new string[10], DeleteArray<string>());
	std::shared_ptr<string> n2(new string[10], [](string* ptr) {delete[] ptr; });
	std::shared_ptr<FILE> n3(fopen("test.cpp","r"), [](FILE* ptr) { fclose(ptr); });
}

那么这是库里面的用法,那么接下来我们就自己实现一个定制删除器的功能。

定制删除器的实现

库中的删除器是在构造函数的参数列表里进行传递,但是库中的实现方法非常的复杂我们就退而求其次,在类的模板里面添加一个参数用来表示删除器,然后修改类里面的Release:

cpp 复制代码
template<class T>
class default_delete
{
public:
	void operator()(T* ptr)
	{
		delete ptr;
	}
};
template<class T, class D = default_delete<T>>
class shared_ptr
{
public:
	// RAII
	// 保存资源
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
	{}
	//template<class D>//这里就是库实现的方法在构造函数上添加模板
	//shared_ptr(T* ptr = nullptr, D del)
	//	:_ptr(ptr)
	//	, _pcount(new int(1))
	//	, _pmtx(new mutex)
	//{}

	// 释放资源
	~shared_ptr()
	{
		Release();
	}
	void Release()
	{
		bool flag = false;
		_pmtx->lock();
		if (--(*_pcount) == 0)
		{
			//delete _ptr;
			_del(_ptr);

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

		if (flag == true)
		{
			delete _pmtx;
		}
	}
private:
	T* _ptr;
	int* _pcount;
	mutex* _pmtx;
	D _del;
}

那么我们就可以用下面的代码来进行测试:

cpp 复制代码
void func5()
{
	YCF::shared_ptr<List_Node, DeleteArray<List_Node>> n2(new List_Node[10]);
}

代码运行的结果如下:

符合我们的预期结果,但是这种实现方法对于lambda表达式就失效了,因为lambda是匿名对象而模板这里需要的是类型,就算添加了decltype也不行因为decltype是运行时得到结果,但是模板编译时才能得到结果,unique_ptr跟我们实现的原理时一样所以lambda表达式放到uniqu_ptr来说所以不行,那么这就是本篇文章的全部内容希望大家能够理解。

相关推荐
爱摸鱼的孔乙己12 分钟前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
Dola_Pan14 分钟前
C语言:数组转换指针的时机
c语言·开发语言·算法
ExiFengs14 分钟前
实际项目Java1.8流处理, Optional常见用法
java·开发语言·spring
paj12345678916 分钟前
JDK1.8新增特性
java·开发语言
IT古董23 分钟前
【人工智能】Python在机器学习与人工智能中的应用
开发语言·人工智能·python·机器学习
繁依Fanyi27 分钟前
简易安卓句分器实现
java·服务器·开发语言·算法·eclipse
烦躁的大鼻嘎43 分钟前
模拟算法实例讲解:从理论到实践的编程之旅
数据结构·c++·算法·leetcode
IU宝1 小时前
C/C++内存管理
java·c语言·c++
湫ccc1 小时前
《Python基础》之pip换国内镜像源
开发语言·python·pip
fhvyxyci1 小时前
【C++之STL】摸清 string 的模拟实现(下)
开发语言·c++·string