【C++】智能指针

前言

上文我们学到了C++11的异常,了解到了C++与C语言处理错误的区别,异常的特点在于抛出与接收。【C++11】异常-CSDN博客

本文我们来学习C++中的下一个功能:智能指针

1.智能指针的使用场景

在上文我们知道了抛异常的知识,抛异常的"抛"这个动作一般来说是当程序出现了错误,抛出错误信息为了让我们解决。这个原本是解决错误的动作,在某些时候却称为了"铸就"错误的是罪魁祸首。

比如:我们知道执行throw,这意味着在这个局部域中throw后面的语句将不再执行,跳过一段又一段程序直到找到匹配的catch时,才会从catch这个语句进行向下执行。那么一个局部域中如果在抛出异常时申请了空间,明明可以正常销毁的,但是却因为抛异常跳过了销毁空间的语句。这就导致一个及其严重的事故:内存泄漏!

在此之前,为了防止出现内存泄漏。我们通常是将抛出的异常再次捕获,执行销毁语句后,将异常重新抛出。但是这种方法并不太好用,所以为了更好的解决这个问题:智能指针诞生了。

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

RAII(Resource Acquisition Is Initialization)是资源获取立即初始化的缩写。RAII是一种资源管理的类的设计思想,其本质就利用类的声明周期来管理资源,类的生命周期没结束时一直保持资源有效,类的生命周期结束时通过析构函数来释放资源。这样就可以保证出现上述情况时,资源可以正常的销毁,避免内存泄漏。(这里的资源可以是内存、文件指针、网络连接、互斥锁等等)

智能指针除了会满足RAII的设计思路,还有考虑到访问资源的便捷性,所以智能指针还会重载operator * / operator -> / operator [ ]等运算符,便于访问。

cpp 复制代码
#include<iostream>
using namespace std;

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

	~Smartptr()
	{
		cout << "~Smartptr" << endl;
		delete[] _ptr;
		_ptr = nullptr;
	}

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

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

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

private:
	T* _ptr;
};


double Divide(int a, int b)
{
	//当除数为0时抛异常
	if (b == 0)
	{
		string s = "除数为0";
		throw s;
	}

	return (double)a / b;
}


void Func()
{
	//使用智能指针,抛异常后也可以正常释放
	Smartptr<int> p1 = new int[10];
	for (int i = 0; i < 10; i++)
		p1[i] = i;

	int a, b;
	cin >> a >> b;
	cout << Divide(a, b)<<endl;

}

int main()
{
	try
	{
		Func();
	}
	catch (const string& errid)
	{
		cout << errid << endl;
	}
}

当Func函数生命周期结束时,Smartptr会自动调用析构函数实现资源是释放。

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

标准库中的智能指针包含在头文件 <memory>

智能指针是用于管理资源的,因此对于智能指针的拷贝来说,我们期望是拷贝出现的智能指针和和原指针共同管理这块资源的,而不是让拷贝出来的资源自己又去管理一个新资源。所以智能指针的拷贝只能是浅拷贝。浅拷贝就会面对一个问题:多次析构,而下面之所以有这么多不同的智能指针正式因为解决多次析构的方法不同导致的。

auto_ptr,这个是C++98中提供的智能指针。它的特点是拷贝时将被拷贝对象的管理权转移给拷贝对象,这会导致拷贝对象悬空,当我们访问拷贝对象时就会报错。这是一个非常不好的设计,许多公司都是明令禁止使用这个智能指针的。

unique_ptr,是C++11中提供的智能指针。其特点是不支持拷贝,只支持移动。如果不需要拷贝的情景下,非常推荐使用这个

share_ptr,是C++11中提供的智能指针。其特点是支持拷贝,也支持移动。如果需要拷贝的场景推荐使用这个。其底层是使用引用计数实现的。

weak_ptr,是C++11中提供的智能指针。虽然叫做智能指针但不太算得上,因为weak_ptr并不支持RAII,也就意味着weak_ptr并不能管理资源。weak_ptr的主要作用在于解决share_ptr的缺陷:循环引用。而循环引用带来的是内存泄漏。

cpp 复制代码
#include<iostream>
#include<memory>
using namespace std;

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

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

	int _day;
	int _month;
	int _year;
};


int main()
{
	auto_ptr<Date> ap1(new Date);
	//拷贝时,拷贝对象安排ap1会被悬空
	auto_ptr<Date> ap2(ap1);
	//此时访问ap1就会报错
	//ap1->_day;


	unique_ptr<Date> up1(new Date);
	//不支持拷贝
	//unique_ptr<Date> up2(up1);
	// 
	//支持移动,但是移动后up1也被悬空
	unique_ptr<Date> up2(move(up1));


	shared_ptr<Date> sp1(new Date);
	//支持拷贝
	shared_ptr<Date> sp2(sp1);
	shared_ptr<Date> sp3(sp2);
	cout << sp1.use_count() << endl;
	cout << sp1->_year << endl;
	cout << sp2->_year << endl;
	cout << sp3->_year << endl;

	// ⽀持移动,但是移动后sp1也悬空,所以使用移动要谨慎 
	shared_ptr<Date> sp4(move(sp1));

}

智能指针析构时默认是使用delete释放资源,这就意味这当资源不是通过new申请时,将资源交给智能指针,析构时就会出问题。

为了解决这一问题,智能指针支持在构造函数里面给一个删除器。删除器其实就是一个可调用对象,我们按照自己想释放资源的方式实现删除器。智能指针接收后就会按照删除里实现的逻辑进行释放资源。

因为使用delete[]释放资源的情况十分常用。使用库里面给我们专门特化了这个情况(share_ptr、unique_ptr均有特化版本)。只需要单独在尖括号里面加一对中括号即可。

值得一提的是,删除器只能在构造时给出,并且后续不能修改。当shared_ptr利用已经存在的对象拷贝构造/赋值给新对象,这个新对象会继承其删除器,即使这并没有显示的写出。而unique_ptr则是将删除器移动给新对象。

cpp 复制代码
	shared_ptr<int[]> sp(new int[10]);
	shared_ptr<Date[]> sp(new Date[10]);
cpp 复制代码
#include<iostream>
#include<memory>
using namespace std;

//总结:使用shared_ptr,建议传Lambda和函数
//	   而使用unique_ptr,建议传Lambda

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


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

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

int main()
{
	//直接传仿函数
	shared_ptr<Date> sp1(new Date[10], Delete<Date>());

	//给出Lambda
	shared_ptr<Date> sp2(new Date[10], [](Date* ptr) {delete[] ptr; });

	
	//值得一提的是unique_ptr与shared_ptr给出删除器的方法是不同的
	//由上面我们可以知道shared_ptr是在构造函数里面给出的
	//而unique_ptr的删除器是要在模板参数中给出,这就有点搞笑了
	unique_ptr<Date, Delete<Date>> up1(new Date[10]);

	//传函数到还可以,但是这个相传Lambda就麻烦了,因为Lambda并非类型而是对象
	//unique_ptr < Date, [](Date* ptr) {delete[] ptr; } > up2(new Date[10]);
	
	//具体要这样写才行,大家了解即可
	auto la = [](Date* ptr) {delete[] ptr; };
	unique_ptr< Date, decltype(la)> up2(new Date[10],la);
}

shared_ptr除了支持用指针构造,还支持使用make_shared直接构造。其好处是避免了空间的碎片化,因为shared_ptr内部成员除了指针还有一个引用计数。使用指针构造只是初始化了指针,还有一个引用计数没有初始化,为此编译器还会再去开辟空间用于初始化引用计数,而要是这种情况过多就会导致空间碎片化,不利用空间利用。而使用make_shared时就会将指针连同引用计数一起开辟,让这两个指针指向的空间连续。

shared_ptr与unique_ptr都支持operator bool的类型转化。当智能指针的对象是一个空对象,没有管理资源,就会返回false,反之返回true。所以我们可以把智能指针的对象给if让其判断是否为空对象。

shared_ptr和unique_ptr的构造函数都是用explicit修饰的,其目的是为了防止普通的指针隐式类型转换为智能指针类型。

cpp 复制代码
int main()
{
	shared_ptr<Date> sp1 = make_shared<Date>(1, 2, 3);
	auto sp2 = make_shared<Date>(1, 2, 3);  //自动推导
	shared_ptr<Date> sp3;

	if (sp2)
		cout << "不是空对象" << endl;
	if (!sp3)
		cout << "是空对象" << endl;


	//报错,不允许隐式类型转化
	shared_ptr<Date> sp4 = new Date(1, 2, 3);
	shared_ptr<Date> sp5 = new Date(1, 2, 3);
	//仅支持显示类型转化
	shared_ptr<Date> sp6 = shared_ptr<Date>(new Date(1, 2, 3));
}

4.智能指针原理

下面将模拟实现三个智能指针的实现思路

auto_ptr和unique_ptr比较简单。auto_ptr会将管理权转移,不建议使用。unique_ptr不支持拷贝,仅支持移动,我们将拷贝禁用掉即可。

重点关注:share_ptr。share_ptr是靠引用计数实现的,share_ptr内部会有一个计数器记录有多少个指针共同管理当前资源。初始化为1,当拷贝时就将计数器++。析构时,当计数器不为1时仅将当前指针赋值为nullptr,当计数器为1时将计数器和资源释放。

cpp 复制代码
//auto_ptr模拟实现
//其特点是管理权转移,不建议使用!

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

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

	auto_ptr<T>& operator=(const auto_ptr<T>& ap)
	{
		//检查是否赋值给自己
		if (_ptr != ap._ptr)
		{
			//释放当前的资源
			if (_ptr)
				delete _ptr;

			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
	}

	~auto_ptr()
	{
		cout << "~auto_ptr" << endl;
		delete _ptr;
		_ptr = nullptr;
	}

	//像指针一样使用
	T* operator->()
	{
		return _ptr;
	}

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

private:
	T* _ptr = nullptr;
};
cpp 复制代码
//unique_ptr模拟实现
//unique_ptr不支持拷贝,仅支持移动

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

	//不支持拷贝(禁用)
	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

	//支持移动
	unique_ptr(unique_ptr<T>&& up)
		:_ptr(up._ptr)
	{
		up._ptr = nullptr;
	}

	unique_ptr<T>& operator=(unique_ptr<T>&& up)
	{
		if (_ptr != up._ptr)
		{
			if (_ptr)
				delete _ptr;

			_ptr = up._ptr;
			up._ptr = nullptr;
		}
	}

	~unique_ptr()
	{
		cout << "~unique_ptr()" << endl;
		delete _ptr;
		_ptr = nullptr;
	}

	//像指针一样使用
	T* operator->()
	{
		return _ptr;
	}

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

private:
	T* _ptr = nullptr;
};
cpp 复制代码
//模拟实现shared_ptr
//其特点是支持拷贝、也支持移动,其底层是通过引用计数实现的
//这里添加了上述我们所将的删除器


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


	//添加删除器
	template<class D>
	shared_ptr(T* ptr,D del)
		:_ptr(ptr)
		,_num(new int(1))
		,_del(del)
	{ }


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

	shared_ptr<T>& operator=(shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			//如果当前引用计数只有1时,直接释放资源
			if (_num == 1)
			{
				delete _ptr;
				delete _num;
				_ptr = _num = nullptr;
			}

			_ptr = sp._ptr;
			*(_num)--;
			_num = sp._num;
			*(_num)++;

			_del = sp._del;
		}

		//返回*this主要是为了实现链式操作:a=b=c
		return *this;
	}

	~shared_ptr()
	{
		if (*(_num) != 1)
		{
			*(_num)--;
			_ptr = nullptr;
		}
		else
		{
			_del(_ptr);
			delete _num;
			_num = _ptr = nullptr;
		}
	}

	int use_count()
	{
		return *_num;
	}

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

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


private:
	T* _ptr;
	int* _num; //引用计数
	//_del的类型是不确定的,利用包装器实现
	function<void(T*)> _del = [](T* ptr) {delete ptr; };
};

5.weak_ptr与shared_ptr

5.1share_ptr的循环引用

目前有两个share_ptr对象:sp1、sp2。这两个的引用计数分别都为1。当sp1指向sp2时,sp2的引用计数为2,当sp2也指向sp1时,sp1的引用计数为2。这时程序结束,sp1进行销毁,引用计数不为1,仅进行减1操作。sp2进行销毁,引用计数不为1,仅进行减1操作。此时销毁操作结束,资源没有被释放,造成了内存泄漏。这就是循环引用带来的问题。

cpp 复制代码
using namespace std;

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
	// 这里改成weak_ptr,当n1->_next = n2时便于赋值

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



int main()
{
	shared_ptr<ListNode> sp1(new ListNode);
	shared_ptr<ListNode> sp2(new ListNode);

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
	//循环引用
	sp1->_next = sp2;
	sp2->_prev = sp1;
	//引用计数增加至2
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
}

我们可以看到结果并没有调用析构函数。则表明循环引用的出现导致了内存泄漏。而C++11为了解决这一问题,设计出了weak_ptr。

5.2weak_ptr

weak_ptr严格意义上并不算智能指针,因为weak_ptr并不支持RAII,也不支持访问资源。weak_ptr在初始化的时候仅支持使用share_ptr初始化。而weak_ptr并不会增加shared_ptr的引用计数,这就完美解决了上面导致循环引用的原因。

cpp 复制代码
struct ListNode
{
	int _data;
	//shared_ptr<ListNode> _next;
	//shared_ptr<ListNode> _prev;
	// 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时 
	// 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了 
	weak_ptr<ListNode> _next;
	weak_ptr<ListNode> _prev;


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



int main()
{
	shared_ptr<ListNode> sp1(new ListNode);
	shared_ptr<ListNode> sp2(new ListNode);

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

	//未使用weak_ptr将导致循环引用
	sp1->_next = sp2;
	sp2->_prev = sp1;

	//未使用weak_ptr引用计数将增加至2
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
}

weak_ptr是没有重载operator*/operator->之类的运算符的,weak_ptr是不支持访问资源的,因为如果当weak_ptr绑定shared_ptr的资源已经释放了,这个时候weak_ptr再去访问就很危险了。

weak_ptr支持expired检查指向的资源是否过期,weak_ptr也可以使用use_count得到shared_ptr的引用计数个数

如果weak_ptr想要访问资源可以使用lock,lock会返回一个管理资源的shared_ptr。如果资源已经释放,则返回一个空对象。如果没有释放则返回一个shared_ptr的对象。通过lock返回的shared_ptr对象访问资源是安全的。

cpp 复制代码
#include<iostream>
#include<memory>
using namespace std;

int main()
{
	shared_ptr<int> sp1(new int(1));
	shared_ptr<int> sp2(sp1);
	weak_ptr<int> wp1(sp2);
	cout << wp1.use_count() << endl; 	//weak_ptr不增加引用计数
	cout << wp1.expired() << endl;      //资源没有被释放,有效

	//通过lock访问资源
	auto sp = wp1.lock();
	*sp += 9;
	cout << *sp << endl;
	cout << *sp1 << endl;


	//资源被释放后,无效
	//引用计数不断的减少,为0时资源释放
	sp1 = make_shared<int>(1);
	cout << wp1.use_count() << endl;
	cout << wp1.expired() << endl;

	sp2 = make_shared<int>(1);
	cout << wp1.use_count() << endl;
	cout << wp1.expired() << endl;

}
相关推荐
明月看潮生25 分钟前
青少年编程与数学 02-019 Rust 编程基础 08课题、字面量、运算符和表达式
开发语言·青少年编程·rust·编程与数学
lwewan28 分钟前
26考研——中央处理器_异常和中断机制(5)
笔记·考研
天天打码1 小时前
Rspack:字节跳动自研 Web 构建工具-基于 Rust打造高性能前端工具链
开发语言·前端·javascript·rust·开源
Petrichorzncu1 小时前
Lua再学习
开发语言·学习·lua
AA-代码批发V哥1 小时前
正则表达式: 从基础到进阶的语法指南
java·开发语言·javascript·python·正则表达式
字节高级特工1 小时前
【C++】”如虎添翼“:模板初阶
java·c语言·前端·javascript·c++·学习·算法
.Vcoistnt1 小时前
Codeforces Round 1024 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划·图论
越甲八千2 小时前
MFC listctrl修改背景颜色
c++·mfc
炯哈哈2 小时前
【上位机——MFC】序列化机制
开发语言·c++·mfc·上位机
蓝莓味柯基2 小时前
Python3正则表达式:字符串魔法师的指南[特殊字符]‍♂️
开发语言·python·正则表达式