目录

C第二十一讲智能指针

C++第二十一讲:智能指针

1.智能指针使用场景分析

智能指针的使用大多发生在容易出现内存泄漏的地方,因为对于某些场景,稍微的操作不当就容易造成内存泄漏,而智能指针的实现,让我们不再需要担心内存泄漏的问题,它能够帮助我们进行内存的释放:

double divided(int a, int b)
{
	if (b == 0)	throw("Divided by zero condition!");
	else return (double)a / (double)b;
}

void Func()
{
	int* array1 = new int[10];
	int* array2 = new int[10];

	try
	{
		int len, time;
		cin >> len >> time;
		cout << divided(len, time) << endl;
	}
	catch (...)
	{
		//1.如果在这里捕获到异常,需要进行delete释放资源
		delete[] array1;
		delete[] array2;
		throw; //异常重新抛出,捕获到什么抛出什么 
	}
	//2.如果没有抛出异常,那么这里也要进行空间的释放
	delete[] array1;
	delete[] array2;
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}

	return 0;
}

上面1、2位置的delete是必不可少的,如果少一个就会导致内存泄漏的问题,而且如果需要释放多个资源,那么还需要手动写出多个delete,这样是很麻烦的,下面我们使用智能指针的知识来解决上面的问题

2.RAII和智能指针的设计思路

什么是RAII:RAII是和资源管理有关的一种编程技术,本质是通过对象的生命周期自动化资源管理,比如智能指针的自动释放资源、文件操作的自动关闭文件、锁管理的自动解锁,防止死锁问题

智能指针除了满足RAII的设计思路外,还要方便资源的访问,所以指针指针还像迭代器一样,重载了operator*/operator->/operator[]等运算符,方便访问资源

下面我们看一下RAII思想的智能指针的简单实现:

namespace YWL
{
	template<class T>
	class Smart_ptr
	{
	public:
		Smart_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~Smart_ptr()
		{
			delete[] _ptr;
		}

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

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

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

	private:
		T* _ptr;
	};
}

此时就不需要再进行析构了,因为当smart_ptr初始化的对象生命周期一到,就会自动执行析构:

double divided(int a, int b)
{
	if (b == 0)	throw("Divided by zero condition!");
	else return (double)a / (double)b;
}

void Func()
{
	YWL::Smart_ptr<int> array1(new int[10]);
	YWL::Smart_ptr<int> array2(new int[10]);

	try
	{
		int len, time;
		cin >> len >> time;
		cout << divided(len, time) << endl;
	}
	catch (...)
	{
		throw; //异常重新抛出,捕获到什么抛出什么 
	}
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}

	return 0;
}

3.C++标准库智能指针的使用

C++标准库中实现的智能指针都被保存在这个头文件中,智能指针有好几种,它们的主要区别在于底层实现的不同

3.1auto_ptr

auto_ptr是C++98实现的智能指针,它的特点是拷贝时将被拷贝对象的资源的管理权交给拷贝对象,这个设计非常糟糕,因为它会导致被拷贝对象悬空,访问报错的问题,所以强烈不建议使用auto_ptr:

#include<memory>

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

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

	int _year;
	int _month;
	int _day;
};

int main()
{
	auto_ptr<Date> ptr1(new Date());
	auto_ptr<Date> ptr2(ptr1);//auto_ptr拷贝时会将ptr1置空,从而造成问题

	//ptr1->_day++;//进程直接终止
	return 0;
}

我们可以查看auto_ptr的模拟实现来观察它的设计:

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

	~auto_ptr()
	{
		if (_ptr) delete _ptr;
	}

	auto_ptr(auto_ptr<T>& autoptr)
		:_ptr(autoptr._ptr)
	{
		autoptr._ptr = nullptr;//转移管理权
	}

	auto_ptr& operator=(auto_ptr<T>& ap)
	{
		//检测是否为⾃⼰给⾃⼰赋值 
		if (this != &ap)
		{
			//释放当前对象中资源 
			if (_ptr)
				delete _ptr;
			//转移ap中资源到当前对象中 
			_ptr = ap._ptr;
			ap._ptr = NULL;
		}
		return *this;
	}

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

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

private:
	T* _ptr;
};

3.2unique_ptr

unique_ptr是C++11设计出来的智能指针,它的特点是不支持拷贝,只支持移动

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

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

	int _year;
	int _month;
	int _day;
};

int main()
{
	unique_ptr<Date> ptr1(new Date());
	//不支持拷贝
	//unique_ptr<Date> ptr2(ptr1);
	//只支持移动,但是移动后ptr1也会悬空
	unique_ptr<Date> ptr2(move(ptr1));
	//ptr1->_day++;

	return 0;
}

它的实现也是相对来说比较简单的:

template<class T>
class unique_ptr
{
public:
	//explicit的作用为防止不必要的类型转换unique_ptr<Date> ptr3 = new Date();
	//可以避免上面代码情况的发生,也就是不支持隐式类型转换,new Date并不是unique_ptr<Date>类型的
	explicit unique_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~unique_ptr()
	{
		if (_ptr) delete _ptr;
	}

	//不支持拷贝,只支持移动
	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr(unique_ptr<T>&& up)
		:_ptr(up._ptr)
	{
		up._ptr = nullptr;
	}

	//不支持拷贝,只支持移动
	unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
	unique_ptr<T>& operator=(unique_ptr<T>&& sp)
	{
		delete _ptr;
		_ptr = sp._ptr;
		sp._ptr = nullptr;
		
		return *this;
	}

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

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

private:
	T* _ptr;
};

3.3shared_ptr

shared_ptr是C++11设计设计出来的智能指针,它的特点是既支持拷贝,又支持移动,使用智能指针时,建议使用shared_ptr,而它的拷贝不是像auto_ptr那样的资源管理权的转移,而是采用了引用计数的方法:

https://i-blog.csdnimg.cn/direct/7520547d7b7f4949b1be26917a354991.png

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

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

	int _year;
	int _month;
	int _day;
};

int main()
{
	shared_ptr<Date> ptr1(new Date());
	shared_ptr<Date> ptr2(ptr1);

	cout << ptr1->_day << endl;//1
	ptr1->_day++;
	cout << ptr2->_day << endl;//2

	return 0;
}

3.3.1删除器

在实现shared_ptr之前,我们需要先强调一下删除器的概念:

智能指针析构默认是delete释放资源,如果不是new出来的资源,交给智能指针管理,析构时就会崩溃,而且对于数组类型的对象,delete释放资源也会造成报错:

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

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

	int _year;
	int _month;
	int _day;
};

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

template<class T>
void DeleteArrayFunc(T* ptr)
{
	delete[] ptr;
}

int main()
{
	//1.对于数组类型的对象,这样写会导致程序崩溃
	//shared_ptr<Date> ptr1(new Date[10]);
	//解决方案一:因为new[]经常使用,所以C++11对于unique_ptr和shared_ptr
	//			  都特化了一份[]的版本
	shared_ptr<Date[]> ptr2(new Date[10]);
	unique_ptr<Date[]> ptr3(new Date[10]);

	//解决方案二:使用删除器
	//unique_ptr和shared_ptr使用删除器的方法不同,这点设计的很麻烦,总而言之:
	//unique_ptr需要指定删除器的类型,还需要指定删除器的指针
	//shared_ptr只需要指定删除其的指针即可
	//1.使用函数指针作删除器
	unique_ptr<Date, void(*)(Date*)> ptr4(new Date[10], DeleteArrayFunc<Date>);
	shared_ptr<Date> ptr5(new Date[10], DeleteArrayFunc<Date>);

	//2.仿函数对象作删除器
	unique_ptr<Date, DeleteArray<Date>> ptr6 (new Date[10]);
	shared_ptr<Date> ptr7(new Date[10], DeleteArray<Date>());

	//3.lambda表达式作删除器
	auto DeleteArray1 = [](Date* ptr) {delete[] ptr; };
	unique_ptr<Date, decltype(DeleteArray1)> ptr8(new Date[10], DeleteArray1);
	shared_ptr<Date> ptr9(new Date[10], [](Date* ptr) { delete[] ptr; });

	//4.实现其他资源管理的删除器 
	shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
		});
	return 0;
}

下面我们再来实现一下shared_ptr:

template<class T>
class shared_ptr
{
public:
	explicit shared_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

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

	shared_ptr& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			//当需要析构时,再进行析构
			if(--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}

		return *this;
	}

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

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

	~shared_ptr()
	{
		if (_ptr && _pcount == 0)
		{
			delete _ptr;
			delete _pcount;
			cout << "~shared_ptr" << endl;
		}
	}

private:
	T* _ptr;
	int* _pcount;
};

带删除器的shared_ptr的实现:

对于删除器需要修改的点在于:

1.默认构造需要通过传入的删除器进行初始化

2.如果没有传入删除器,那么应该使用什么进行删除

3.析构函数里需要使用到删除器

template<class T>
class shared_ptr
{
public:
	explicit shared_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

	//删除器需要一个类型,那么我们就使用函数模板
	template<class D>
	shared_ptr(T* ptr, D del)
		:_ptr(ptr)
		,_pcount(new int(1))
		,_del(del)
	{}

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

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

	shared_ptr& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			release();

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

		return *this;
	}

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

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

	~shared_ptr()
	{
		release();
	}

private:
	T* _ptr;
	int* _pcount;

	function<void(T*)> _del = [](T* ptr) {delete ptr; };
};

3.4标准库中的智能指针问题

3.4.1operator bool(shared_ptr && unique_ptr)

C++标准库中支持直接使用if(ptr1)的方式来判断ptr是否为空,也就是是否指向了一块有效的空间,其实应该的使用方法为:ptr1.operator bool(),但是简化为可以直接使用了

3.4.2make_shared(shared_ptr)

我们可以使用shared_ptr sp2 = make_shared(2024, 9, 11);来直接构造一个智能指针,效果和shared_ptr sp1(new Date(2024, 9, 11));是相同的

3.4.3use_cout

支持ptr1.use_cout()来获取ptr中有多少个指针指向这块资源

4.weak_ptr

4.1shared_ptr的循环引用问题

https://i-blog.csdnimg.cn/direct/5ebf72d582d847358e81eb4320365731.png

struct ListNode
{
	int _data;
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;

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

int main()
{
	shared_ptr<ListNode> ptr1(new ListNode());
	shared_ptr<ListNode> ptr2(new ListNode());

	cout << ptr1.use_count() << endl;//1
	cout << ptr2.use_count() << endl;//1

	ptr1->_next = ptr2;
	ptr2->_prev = ptr1;

	cout << ptr1.use_count() << endl;//2
	cout << ptr2.use_count() << endl;//2
	return 0;
}

4.2weak_ptr

使用weak_ptr可以解决循环引用的问题:

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

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

int main()
{
	shared_ptr<ListNode> ptr1(new ListNode());
	shared_ptr<ListNode> ptr2(new ListNode());

	cout << ptr1.use_count() << endl;//1
	cout << ptr2.use_count() << endl;//1

	ptr1->_next = ptr2;
	ptr2->_prev = ptr1;

	cout << ptr1.use_count() << endl;//1
	cout << ptr2.use_count() << endl;//1
	return 0;
}

weak_ptr的解决原理为:weak_ptr初始化的指针能够指向shared_ptr指向的那一块空间,而且不增加shared_ptr的引用计数,也就是说,此时shared_ptr的next和prev指针的使用不会增加引用计数了,所以就可以顺利进行析构了

4.2.1weak_ptr的使用

weak_ptr并没有重载operator* 和 operator->,所以它并不能够进行资源的访问,但是它支持调用lock返回一个管理资源的shared_ptr,如果资源已经释放,那么shared_ptr返回空对象,否则就可以正常进行资源的访问:

int main()
{
	std::shared_ptr<string> sp1(new string("111111"));
	std::shared_ptr<string> sp2(sp1);
	std::weak_ptr<string> wp = sp1;

	//1.expired可以检查资源是否过期,过期返回1,否则返回0
	cout << wp.expired() << endl;//0
	cout << wp.use_count() << endl;//2,wp的指向不增加引用计数

	//2.使用lock函数可以进行资源的访问
	//std::shared_ptr<string> sp3 = wp.lock();
	auto sp3 = wp.lock();
	cout << *sp3 << endl;//111111
	cout << wp.use_count() << endl;//3,新增了一个指向
	*sp3 += "###";
	return 0;
}

weak_ptr的实现:

template<class T>
class weak_ptr

{
public:
	weak_ptr()
	{}

	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get())//get函数可以获取sp指向的那一块空间的地址
	{}

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

private:
	T* _ptr = nullptr;
};

5.shared_ptr的线程安全问题

这个在后面会讲到,后面再说

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

https://i-blog.csdnimg.cn/direct/9a2fa6a70e614214b0b3206dd4f27fd0.png