1.为什么需要智能指针

在异常的时候我们说过这里的问题,下面这种写法抛异常了会造成内存泄漏的问题。

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

	return a / b;
}

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	int* p2 = nullptr;

	cout << div() << endl;


	delete[] p1;
	delete[] p2;
}

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

	return 0;
}

前面我们的处理方式是把异常捕捉一下然后重新抛出。

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	int* p2 = new int[10];

	try {
		cout << div() << endl;
	}
	catch (...)
	{
		delete[] p1;
		delete[] p2;
		throw;
	}

	delete[] p1;
	delete[] p2;
}

但是这样的处理方式并不好。
第一代码很丑。
第二要注意到new的时候也会抛除异常。
p1这里抛异常倒是没事,如果抛异常直接跳转到main的catch进行捕捉下面都不会影响。但是p2抛了异常呢?p1已经成功申请空间了,如果不处理又会造成内存泄漏!

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	int* p2 = nullptr;

	try {
		p2 = new int[10];

		try {
			cout << div() << endl;
		}
		catch (...)
		{
			delete[] p1;
			delete[] p2;
			throw;
		}
	}
	catch(...)
	{
		delete[] p1;
		//...
 	}

	delete[] p1;
	delete[] p2;

}

如果又有p3,p4呢? 这里最本质的问题就是new本身就会抛异常。
这里真正的解决方式就是智能指针

我们看看智能指针内部是怎么实现的。

template<class T>
class Smartptr
{
public:
	Smartptr(T* ptr)
		:_ptr(ptr)
	{}

	~Smartptr()
	{
		//下面是[]这里暂时写成这个样子
		delete[] _ptr;
		cout << _ptr << endl;
	}
private:
	T* _ptr;
};


void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	Smartptr<int> sp1(p1);
	int* p2 = new int[10];
	Smartptr<int> sp2(p2);


	cout << div() << endl;

	//不需要主动释放,无论是谁抛异常都能释放
	//delete[] p1;
	//delete[] p2;

}

在这里插入图片描述

抛异常栈帧会正常销毁,栈帧销毁这些对象就会出作用域,自定义类型出作用域就会调用析构函数,就会释放资源。

下面这里是把一个new出来的资源交给智能指针的对象,出了作用域就会自动把资源释放。

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int[10];
	Smartptr<int> sp1(p1);
	int* p2 = new int[10];
	Smartptr<int> sp2(p2);

	cout << div() << endl;

	//不需要主动释放,无论是谁抛异常都能释放
	//delete[] p1;
	//delete[] p2;

}

有了智能指针抛异常带来的内存泄漏的问题就得到极大解决。

这里也可以直接把new出来的资源交给智能指针

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	//int* p1 = new int[10];
	//Smartptr<int> sp1(p1);
	//int* p2 = new int[10];
	//Smartptr<int> sp2(p2);

	Smartptr<int> sp1(new int[10]);
	Smartptr<int> sp2(new int[10]);

	cout << div() << endl;
}

智能指针,也想解引用一下有什么方式可以处理一下呢?
可以重载一下。

template<class T>
class Smartptr
{
public:
	//RAII
	//构造---保存资源
	Smartptr(T* ptr)
		:_ptr(ptr)
	{}

	//析构---释放资源
	~Smartptr()
	{
		//下面是[]这里暂时写成这个样子
		delete[] _ptr;
		cout << _ptr << endl;
	}

	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
};

下面就可以像普通指针一样去解引用等

void Func()
{
	//直接把new出来的资源交给智能指针
	Smartptr<int> sp1(new int[10]);
	Smartptr<int> sp2(new int[10]);

	*sp1 = 10;
	sp1[0]--;
	cout << *sp1 << endl;
	
	cout << div() << endl;
}

在这里插入图片描述

2.智能指针原理

智能指针的原理:

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

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

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

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

3.智能指针的使用以及问题

智能指针的使用我们已经见识到了。那它有没有什么问题呢?
就以我们刚才写的智能指针为例。

template<class T>
class Smartptr
{
public:
	//RAII
	//保存资源
	Smartptr(T* ptr)
		:_ptr(ptr)
	{}

	//释放资源
	~Smartptr()
	{
		delete _ptr;
		cout << _ptr << endl;
	}

	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
};

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

	return 0;
}

运行奔溃了,原因是什么?

在这里插入图片描述

是不是浅拷贝,析构了两次的原因。因为我们没写拷贝构造。

那我们写个拷贝构造进行深拷贝解决一下?
不可以。不能深拷贝,因为智能指针模仿的就是原生指针的行为,期望指向的就是同一个。

这里就不得不提到C++指针智能的发展历史。

3.1auto_ptr

auto_ptr文档
C++98 出现了auto_ptr,它解决这里的方式:资源管理权转移

int main()
{
	//Smartptr<int> sp1(new int);
	//Smartptr<int> sp2(sp1);
	
	auto_ptr<int> ap1(new int);
	auto_ptr<int> ap2(ap1);
	
	return 0;
}

在这里插入图片描述
它的思路是这样的,两个一起管这个资源有问题,那我不管了你管,一个人管。

这种方式会导致什么问题?
会导致不知道的人对空指针访问的问题!—>对象悬空

在这里插入图片描述
下面我们简单模拟实现一下

namespace bit
{
	template<class T>
	class auto_ptr
	{
	public:
		//保存资源
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		//释放资源
		~auto_ptr()
		{
			//delete[] _ptr;
			delete _ptr;
			cout << _ptr << endl;
		}

		//拷贝构造 -- 对象悬空
		//这里不能加const
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}

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

		T& operator[](size_t pos)
		{
			return _ptr[pos];
		}
	private:
		T* _ptr;
	};
}
int main()
{

	bit::auto_ptr<int> ap1(new int);
	bit::auto_ptr<int> ap2(ap1);

	(*ap2)++;
	(*ap1)++;

	return 0;
}

在这里插入图片描述

对于这个智能指针不要用!

在后面C++的发展历史出现其他的智能指针。

但中间不得不提boost库

在这里插入图片描述

boost库出现的智能指针
scoped_ptr
shared_ptr/weak_ptr

C++11也搞了智能指针。
C++11 unique_ptr 就是抄的 scoped_ptr
C++11 也有 shared_ptr/weak_ptr

3.2unique_ptr

先看库里unique_ptr解决方案。

#include<memory>

int main()
{

	std::unique_ptr<int> up1(new int);
	std::unique_ptr<int> up2(up1);

	return 0;
}

unique_ptr解决方案非常简单粗暴。 --> 防拷贝,因此也叫做唯一指针
直接把拷贝构造给删了。同样赋值重载也是一样被删了。

在这里插入图片描述

下面简单实现一下

template<class T>
class unique_ptr
{
public:
	//保存资源
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}

	//释放资源
	~unique_ptr()
	{
		delete _ptr;
		cout << _ptr << endl;
	}

	//拷贝构造 
	unique_ptr(const unique_ptr<T>& up) = delete;

	//赋值重载
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;


	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
};

但是不让拷也不是办法啊,因此有了下面的指针。

3.3shared_ptr

shared_ptr - - 共享指针。

int main()
{
	std::shared_ptr<int> sp1(new int(0));
	std::shared_ptr<int> sp2(sp1);
	(*sp2)++;
	(*sp1)++;
	
	cout << *sp1 << endl;
	cout << *sp2 << endl;
	
	return 0;
}

在这里插入图片描述
现在既可以拷贝,也没有悬空问题!

它是怎么实现的呢?
一起指向,但增加一个引用计数

下面看看shared_ptr是如何实现。
首先第一个问题引用计数如何实现

在私有成员里直接搞一个count成员变量可不可以?

在这里插入图片描述

不可以。因为这里你是你的计数,我是我的计数,咋可能可以。
引用计数代表有多少个智能指针在管理这块资源,减到0的时候才会把资源释放。

在这里插入图片描述

搞成静态的可不可以?

在这里插入图片描述

看似可以所有对象都共用一个,实际会有大问题!就如下面的情况
再来一个sp4呢?
引用计数要加到4,那sp4析构引用计数减1,sp4指向的这块资源要不要释放?

在这里插入图片描述

因为这个引用计数属于这个类也属于这个类所有对象,因此不好判断!

这里一个资源就需要一个引用计数。

搞一个静态数组可以吗?
也是不可以的。要找对应资源的引用计数也麻烦。你怎么知道该资源引用计数在数组那个地方。

那搞一个map把资源和引用计数一一对应咋样?
这个想法是可以的。所有对象都共享一个静态的map,把指针和计数进行关联。

这里还有一种思路,也是库里的解决方法。
分配资源的时候就开一个计数,这个计数开到堆上,用一个指针指向这个计数就可以了。
在这里插入图片描述

当来了一个对象和其他对象指向同一份资源时直接把引用计数++就可以了,析构的时候把引用计数- -,等到引用计数到0的时候在释放资源。
这种方式就不用向map那种还需要查找,插入,删除等等。
每个对象既需要指向资源也需要指向这个引用计数。

根据这里分析把代码先写一下

template<class T>
class shared_ptr
{
public:
	//保存资源
	shared_ptr(T* ptr)
		:_ptr(ptr)
		:_pcount(new int(1))//初始引用计数给1
	{}

	//释放资源
	~shared_ptr()
	{
		//先--引用计数,减到0在释放资源
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
			cout << _ptr << endl;
		}
	}
	
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		//引用计数++
		++(*_pcount);
	}

	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
	int* _pcount;
};
int main()
{
	bit::shared_ptr<int> sp1(new int(0));
	bit::shared_ptr<int> sp2(sp1);

	(*sp2)++;
	(*sp1)++;

	cout << *sp1 << endl;
	cout << *sp2 << endl;

	bit::shared_ptr<int> sp3(new int(10));

	return 0;
}

可以看到指向不同资源的两个对象,出作用域析构的时候互不影响。

在这里插入图片描述

拷贝构造没问题了,赋值重载怎么写?注意写的时候很容易出现问题。

首先不能自己给自己赋值。但还要考虑一点的时候可能前面出现拷贝的情况多个对象指向同一块资源,因此指向同一块资源的就不需要赋值重载。
其次不能直接就去释放资源,一定要记得先减减引用计数,减到0的时候才释放资源。

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	//sp1  sp2(sp1) 
	//sp1=sp1  sp1=sp2  都直接返回
	if (_ptr != sp._ptr)
	{
		//--引用计数,到0才能释放资源
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}

		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);//不用忘记给新指向的引用计数++
	}
	return *this;
}
int main()
{
	bit::shared_ptr<int> sp1(new int(0));
	bit::shared_ptr<int> sp2(sp1);
	bit::shared_ptr<int> sp3(sp1);
	(*sp2)++;
	(*sp1)++;

	cout << *sp1 << endl;
	cout << *sp2 << endl;

	bit::shared_ptr<int> sp4(new int(10));
	bit::shared_ptr<int> sp5(sp4);

	sp1 = sp4;

	return 0;
}

在这里插入图片描述
现在我们学了这三个智能指针总结一下:
无论无何都不用auto_ptr
不想要拷贝,也不需要被拷贝用unique_ptr
需要被拷贝,用shared_ptr,但是要小心循环引用!

shared_ptr除了循环引用的问题,还有线程安全的问题。

3.3.1shared_ptr的线程安全

先把上面shared_ptr代码改一下再看这个问题

template<class T>
class shared_ptr
{
public:
	//保存资源
	shared_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

	//释放资源
	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		//引用计数++
		++(*_pcount);
	}

	void Release()
	{
		//--引用计数,到0才能释放资源
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//sp1=sp1  sp2(sp1) 都直接返回
		if (_ptr != sp._ptr)
		{
			Release();

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);//不用忘记给新指向的引用计数++
		}
		return *this;
	}
	
	//获取引用计数
	int use_count()
	{
		return *_pcount;
	}
	
	T* get()
	{
		return _ptr;
	}

	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
	int* _pcount;
};

有没有可能一个资源有两个线程在管?
非常有可能。
我们看下面一段代码,两个线程都对同一个智能指针进行疯狂拷贝和析构,看看最后引用计数是多少。

void test_shared_ptr()
{
	shared_ptr<int> sp1(new int(1));
	int n = 10000;

	thread t1([&]()
	{
		for (int i = 0; i < n; ++i)
		{
			shared_ptr<int> sp2(sp1);
		}
	});

	thread t2([&]()
	{
		for (int i = 0; i < n; ++i)
		{
			shared_ptr<int> sp3(sp1);
		}
	});

	t1.join();
	t2.join();
	cout << sp1.use_count() << endl;
	cout << sp1.get() << endl;
}

现在两个线程同时对同一个引用计数++,- -有没有什么问题?
正常情况下打印sp1引用计数是1。

看下面三种情况每次都出现问题,要不就是少++正常- -导致的问题,要不就是正常++少- -导致的问题
在这里插入图片描述
这个问题如何解决呢?
是不是必须保证引用计数++,- -是线程安全的!
这里我们加锁!或者使用原子类型的操作。
能不能这样直接搞一个锁对象?

在这里插入图片描述
并不能,因为要++,- -必须在同一把锁上,像这样写在拷贝构造直接把锁拷贝过来就不是同一把锁了,并且锁还不允许拷贝。
在这里插入图片描述
因此只能在堆上开,然后指向相同资源也都可以指向同一个锁对象。
在这里插入图片描述

template<class T>
class shared_ptr
{
public:
	//保存资源
	shared_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
		,_pmtx(new mutex)
	{}

	//释放资源
	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
		,_pmtx(sp._pmtx)
	{
		_pmtx->lock();
		//引用计数++
		++(*_pcount);
		_pmtx->unlock();
	}

	//注意Release这里有些问题
	void Release()
	{
		_pmtx->lock();
		//--引用计数,到0才能释放资源
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
			flag = true;
		}
		
		_pmtx->unlock();
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//sp1=sp1  sp2(sp1) 都直接返回
		if (_ptr != sp._ptr)
		{
			Release();

			_ptr = sp._ptr;
			_pcount = sp._pcount;

			_pmtx->lock();
			++(*_pcount);//不用忘记给新指向的引用计数++
			_pmtx->unlock();

		}
		return *this;
	}
	
	//获取引用计数
	int use_count()
	{
		return *_pcount;
	}
	
	T* get()
	{
		return _ptr;
	}

	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
	int* _pcount;
	mutex* _pmtx;
};

加锁之后就没问题了。

在这里插入图片描述

Release这里有没有看出什么问题?

//注意Release这里有些问题
void Release()
{
	_pmtx->lock();
	//--引用计数,到0才能释放资源
	if (--(*_pcount) == 0)
	{
		delete _ptr;
		delete _pcount;
		flag = true;
	}	
	_pmtx->unlock();
}

没错,锁没释放!
正确写法如下

void Release()
{
	//给个标记位
	bool flag = false;
	_pmtx->lock();
	//--引用计数,到0才能释放资源
	if (--(*_pcount) == 0)
	{
		delete _ptr;
		delete _pcount;
		flag = true;
	}
	
	_pmtx->unlock();
	
	//不能再锁里面就把锁给释放掉了
	if (flag == true)
	{
		delete _pmtx;
	}
}

问:这个flag有没有线程安全的问题?
没有!每个线程都有独立的栈帧结构。这个flag在Release里是局部变量,而每个栈帧都有Release。对于一些公共资源会有线程安全的问题。

问:shared_ptr是不是线程安全的?
shared_ptr本身是线程安全的!指的是拷贝,析构的引用计数的++、- -是线程安全的。因为锁在内部。
shared_ptr管理资源的访问不是线程安全的!需要用的地方自行保护。

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

void test_shared_ptr()
{
	shared_ptr<Date> sp1(new Date);
	int n = 10000;

	thread t1([&]()
	{
		for (int i = 0; i < n; ++i)
		{
			shared_ptr<Date> sp2(sp1);
			sp2->_year++;
			sp2->_month++;
			sp2->_day++;		
		}
	});

	thread t2([&]()
	{
		for (int i = 0; i < n; ++i)
		{
			shared_ptr<Date> sp3(sp1);
			sp3->_year++;
			sp3->_month++;
			sp3->_day++;
		}
	});

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

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

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

在这里插入图片描述

这里可以在外面加锁进行保护。

void test_shared_ptr()
{
	shared_ptr<Date> sp1(new Date);
	int n = 10000;
	mutex mtx;

	thread t1([&]()
	{
		for (int i = 0; i < n; ++i)
		{
			shared_ptr<Date> sp2(sp1);
			mtx.lock();
			sp2->_year++;
			sp2->_month++;
			sp2->_day++;		
			mtx.unlock();
		}
	});

	thread t2([&]()
	{
		for (int i = 0; i < n; ++i)
		{
			shared_ptr<Date> sp3(sp1);
			mtx.lock();
			sp3->_year++;
			sp3->_month++;
			sp3->_day++;
			mtx.unlock();
		}
	});

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

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

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

在这里插入图片描述

3.3.2shared_ptr的循环引用

shared_ptr有一个死穴就是循环引用,下面我们模拟实现一下。

以前我们玩这块代码的时候就是申请然后手动delete释放

struct ListNode
{
	int val;
	ListNode* _next;
	ListNode* _prev;

};

void test_shared_ptr2()
{
	ListNode* n1 = new ListNode;
	ListNode* n2 = new ListNode;

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

	delete n1;
	delete n2;
}

现在有了智能指针就不需要自己手动释放了。就可以这样玩了

struct ListNode
{
	int val;
	//ListNode* _next;
	//ListNode* _prev;
	
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;

	//这里主要是看释放没有
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

void test_shared_ptr2()
{
	//ListNode* n1 = new ListNode;
	//ListNode* n2 = new ListNode;

	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);

	//n1,n2改成shared_ptr这里会出现类型不匹配的问题
	//一个是智能指针自定义类型对象,一个是普通类内置类型对象
	//因此上面也改成智能指针对象,就可以了
	n1->_next = n2;
	n2->_prev = n1;

	//delete n1;
	//delete n2;
}

改完为什么不释放了?

在这里插入图片描述

把next,prev屏蔽掉就可以释放了。

在这里插入图片描述

原因是构成了循环引用
刚开始都是只有一个指向,引用计数都是1
在这里插入图片描述
加上两句代码,n2所指向的资源除了n2在管理还有_next在管理,因此引用计数是2。n1所指向的资源也是一样除了n1在指向还有_prve在指向,因此引用计数也是2。
在这里插入图片描述
等到n1,n2出了作用域它们俩就销毁了,对应的引用计数-1
n1所指向的资源就只有_prev在管,n2所指向的资源就只有_next在管。
在这里插入图片描述
然后最后就没释放导致内存泄漏!

n1、n2是局部对象出了作用域就销毁那_next、_prve什么时候销毁呢?
_next、_prve都是ListNode类对象的的成员,一个类对象的成员什么时候销毁呢?
是不是对象销毁它的成员才会销毁。

所以_prev什么时候销毁主要看它这个结点类的对象什么时候销毁,但是这个结点类对象是由_next所管,因此_next什么时候销毁这个结点类对象什么时候销毁。同样_next什么时候销毁也看它这个结点对象什么时候销毁,它这个结点对象什么时候销毁就看_prev什么时候销毁。
因此这就造成了一个死循环,也叫做循环引用!这也是shared_ptr一个死穴。

在这里插入图片描述
这里如果只有一个指向就没有这个问题。

在这里插入图片描述

出了作用域n1、n2都释放了。n2引用计数减到1。n1引用计数减到0,然后所指向的资源就释放了,_next也就被释放了因此n2引用计数减到0,所有n2所指向的资源也就被释放了。

在这里插入图片描述

那这个循环引用的问题到底该如何解决呢?
这个时候weak_ptr就要上场了。
可以认为weak_ptr是shared_ptr的小弟!

3.4weak_ptr

weak_ptr 可以指向资源,访问资源,不参与资源的管理也就是不增加引用计数

可以看到weak_ptr构造函数只有无参构造、拷贝构造、还有shared_ptr拷贝构造。并没有指针构造!也就是说不支持RAII并且并不会把资源交给它管理。并且赋值也有shared_ptr的赋值。

在这里插入图片描述
在这里插入图片描述
因此要想不增加引用计数_next,_prev就不能用shared_ptr

struct ListNode
{
	int val;
	//ListNode* _next;
	//ListNode* _prev;

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

	//可以指向资源,访问资源,不参与资源的管理也就是不增加引用计数
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;

	//这里主要是看释放没有
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

void test_shared_ptr1()
{
	//ListNode* n1 = new ListNode;
	//ListNode* n2 = new ListNode;

	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);


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

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	
	//delete n1;
	//delete n2;
}

可以看到weak_ptr解决了循环引用的问题。

在这里插入图片描述

刚才用的是库的,现在自己简单实现一个

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}

private:
	T* _ptr;
};

注意sharde_ptr的get要加一个cosnt不然会报错,const对象不能调用非const的普通对象的。

在这里插入图片描述

在这里插入图片描述

weak_ptr不参与资源的管理,万一资源被释放了怎么办呢?其实库里实现的更复杂,下面这个接口就可以帮我们解决这个问题。

在这里插入图片描述

weak_ptr虽然它不参与资源的管理,但是它也可以存引用计数。shared_ptr实现要改一改它们两个释放只把资源给释放了,但是不要释放引用计数。然后weak_ptr查看资源是否被释放了,就去看看shared_ptr引用计数如果大于0说明资源还可以访问,引用计数到0了就不要访问了。- -weak_ptr自己的引用计数(为了防止多个weak_ptr指向同一个资源),如果自己的应用计数到0 就把所有的引用计数都释放掉。
在这里插入图片描述

库里还提供一个make_shared接口,它类似于make_pair,它可以把资源引用计数锁都开在一块,以后遇到再说。
在这里插入图片描述

在这里插入图片描述

4.定制删除器

前面所说的智能指针都存在这样一个问题。
new一个对象出来没问题,new[ ]出来一堆呢?

int main()
{
	std::shared_ptr<int> sp1(new int(0));
	std::shared_ptr<int> sp2(new int[10]);

	return 0;
}

以前说过不匹配,可能会出现问题。
内置类型可能没啥事。

在这里插入图片描述

自定义类型呢?

int main()
{
	std::shared_ptr<int> sp1(new int(0));
	std::shared_ptr<int> sp2(new int[10]);
	std::shared_ptr<string> sp3(new string[10]);
	return 0;
}

可以看到直接就奔溃了。

在这里插入图片描述
崩溃原因是new[ ]和delete不匹配。

这里解决方法就是定制删除器

可以看到shared_ptr就给了一个类似仿函数对象或者说是可调用对象:函数对象、函数指针、lambda。

在这里插入图片描述

定制删除器也就是说你可以写一个你需要的删除方式的可调用对象传进行,然后会用你传过去的定制删除器这种方法删除

下面看看怎么用

template<class T>
struct DeleteArrat
{
	void operator()(T* ptr)
	{
		delete[] ptr;

		//这里只是为了方便看
		cout << "delete []" << ptr << endl;
	}
};

int main()
{
	std::shared_ptr<int> sp2(new int[10],DeleteArrat<int>());
	std::shared_ptr<string> sp3(new string[10], DeleteArrat<string>());

	return 0;
}

这里就会走我们传过去的定制删除器进行删除。

在这里插入图片描述

这里除了仿函数还可以用lambda,用着更舒服一些。

int main()
{
	std::shared_ptr<string> sp3(new string[10], DeleteArrat<string>());
	std::shared_ptr<string> sp4(new string[10], [](string* ptr) {delete[] ptr; });
	//甚至文件也可以
	std::shared_ptr<FILE> sp5(fopen("text.txt", "w"), [](FILE* ptr) {fclose(ptr); });

	return 0;
}

现在我们可以这样玩是因为库里构造函数可以这样传给了这样的模板,但是我们自己写的玩不了,库里shared_ptr写了5、6个类,我们就写了一个类。它的引用计数专门交给一个类管然后可以把这个删除器层层往下传。

我们自己搞定制删除器,写到这里就写不下去了,为什么呢?

template<class D>
shared_ptr(T* ptr = nullptr,D del)
	:_ptr(ptr)
	, _pcount(new int(1))
	, _pmtx(new mutex)
	,
{}

因为D是这个构造函数的,但是这个D对象定制删除器是要传给Release去使用释放资源的,怎么传给Release呢?只能是在类中在定义一个私有成员_del,然后_del(del)才能传过去。但是这个_del定义不出来,因为这个D是你这个成员函数的模板参数并不是整个类的参数,整个类也用不上。

这里我们只是简单模仿一下,把D放在这个类的模板参数上

template<class T>
struct default_delete
{
	void operator()(T* ptr)
	{
		delete ptr;
	}
};


template<class T,class D=default_delete<T>>
class shared_ptr
{
public:
	//保存资源
	shared_ptr(T* ptr=nullptr)
		:_ptr(ptr)
		,_pcount(new int(1))
		,_pmtx(new mutex)
	{}


	//释放资源
	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
		,_pmtx(sp._pmtx)
	{
		_pmtx->lock();
		//引用计数++
		++(*_pcount);
		_pmtx->unlock();
	}

	void Release()
	{
		bool flag = false;
		_pmtx->lock();
		//--引用计数,到0才能释放资源
		if (--(*_pcount) == 0)
		{
			//delete _ptr;
			
			_del(_ptr);
			delete _pcount;
			flag = true;
		}		
		_pmtx->unlock();

		if (flag == true)
		{
			delete _pmtx;
		}

	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//sp1=sp1  sp2(sp1) 都直接返回
		if (_ptr != sp._ptr)
		{
			Release();

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp._pmtx;

			_pmtx->lock();
			++(*_pcount);//不用忘记给新指向的引用计数++
			_pmtx->unlock();

		}
		return *this;
	}
	
	//获取引用计数
	int use_count() const
	{
		return *_pcount;
	}
	
	T* get() const
	{
		return _ptr;
	}

	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t pos)
	{
		return _ptr[pos];
	}
private:
	T* _ptr;
	int* _pcount;
	mutex* _pmtx;

	D _del;
};
struct Delete_array
{
	void operator()(string* ptr)
	{
		delete[] ptr;
	}
};

int main()
{
	//自己写的这样传
	bit::shared_ptr<string, DeleteArrat<string>> sp3(new string[10]);
	
	//这里传lambda就有问题.因为这里只能传类型,而现在传的是匿名函数对象
	//bit::shared_ptr<string, [](string* ptr) {delete[] ptr; } > sp4(new string[10]);
	// 用上decltype也不行,decltype虽然可以推导对象的类型,但是是编译时推导,而这里直接在编译时就要传这个东西
	//bit::shared_ptr<string, decltype([](string* ptr) {delete[] ptr; }) > sp4(new string[10]);

	//可以写一个仿函数
	bit::shared_ptr<string, Delete_array > sp4(new string[10]);
	
	return 0;
}

unique_ptr也可以定制删除器,它的实现和我们是一样的。

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐