C++智能指针

前言

这一节讲智能指针

1. 为什么需要智能指针

上一节我们就讲过,因为new有抛异常,所以可能会抛异常跳过了delete,导致内存泄漏,所以采用智能指针
智能指针采用的技术是RALL,就是是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术

2. 智能指针的简单实现

struct Date
{
	Date(int year=0,int month=0,int day=0)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	~Date()
	{
		cout << "~Date()" << endl;
	}
	int _year;
	int _month;
	int _day;
};

template<class T>
class my_ptr
{
public:
	my_ptr(T*ptr=nullptr)
		:_ptr(ptr)
	{}
	~my_ptr()
	{
		delete _ptr;//会调用_ptr的析构函数
		_ptr = nullptr;
	}
private:
	T* _ptr;
};

void func()
{
	my_ptr<Date> p(new Date);
	throw 1;
}

int main()
{
	try
	{
		func();
	}
	catch (...)
	{
		cout << "ssss" << endl;
	}
	return 0;
}

在这里插入图片描述

void func()
{
	my_ptr<Date> p(new Date);
	//throw 1;
}

在这里插入图片描述
其实智能指针就是将你开辟好的指向空间的指针传给一个类,用这个类来构造,然后重载进行使用,最后会自动调用析构函数析构
看这个我们就知道了,不管是程序正常结束,不抛异常,还是抛异常了,这个类都是会析构的

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

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

再定义两个这种的函数,就可以像普通指针那样使用了
但是智能指针最重要的问题还是拷贝构造问题
根据普通指针的逻辑,智能指针的拷贝应该是浅拷贝,但是,浅拷贝的话,同一个地方又会析构两次,有问题,所以我们要重点解决这个问题

3. 智能指针的发展以及使用

库里面的智能指针在memory这个头文件里

3.1 auto_ptr

它解决拷贝问题就比较垃圾,它是直接权限转移,意思是拷贝过去,原来的就不能正常使用了

auto_ptr<Date> p1(new Date);
auto_ptr<Date> p2(p1);
p2->_day++;

在这里插入图片描述

auto_ptr<Date> p1(new Date);
auto_ptr<Date> p2(p1);
p2->_day++;
p1->_day++;

在这里插入图片描述

my_ptr(const my_ptr&ptr)
	:_ptr(ptr._ptr)
{
	ptr._ptr = nullptr;
}

因为它的构造函数是这样使用的,所以就无法使用以前得了
这个智能指针比较垃圾,最好不要使用

3.2 unique_ptr

unique_ptr这个智能指针就是明确规定了不能使用拷贝构造和赋值
在这里插入图片描述
这个智能指针就是说,不能拷贝和赋值,因为它底层实现就直接把拷贝和赋值deletel

my_ptr(const my_ptr& ptr) = delete;
my_ptr&operator=(const my_ptr& ptr) = delete;

就相当于这样

3.3 shared_ptr

shared_ptr就支持拷贝构造和赋值,这是因为这个模版底层是采用了引用计数的技术的,就是对一块开辟的空间计数,看有几个指向它,当有0个指向它的时候就是释放的时候

shared_ptr<Date> p1(new Date);
shared_ptr<Date> p2(p1);

在这里插入图片描述

3.4 删除器

接下来我们再来看一下删除器,就是这么释放内存的问题,系统默认的是delete,但有些东西是用delete无法释放的,比如开辟一个数组,要用delete[],打开一个文件,要用fclose

unique_ptr<Date> p1(new Date[5]);

在这里插入图片描述
这时候我们就要加入删除器这个东西了,相当于以前加入的仿函数
在这里插入图片描述
unique_ptr的删除器是作为模版参数传入的,auto_ptr我们就不再介绍了,因为好多公司都禁止使用
作为模版参数的话,那么就要传入一个类型,而不是对象

template<class T>
struct del
{
	void operator()(T* d)
	{
		delete[]d;
	}
};
unique_ptr<Date, del<Date>> p1(new Date[5]);

在这里插入图片描述
就这样传入删除器就可以删除其他类型的了

	unique_ptr<Date[]> p1(new Date[5]);

在这里插入图片描述
但是针对delete[]的情形,模版有一个特化,就是第一个模版参数传入Date[]的时候,就会特化一个模版。用delete,而且shared_ptr也是这样的

shared_ptr<Date> p1(new Date[5], [](Date* p) {delete[]p; });

在这里插入图片描述
还要注意的就是,shared_ptr的删除器的传入是作为构造函数的参数传入的,所以传入的是对象,当然针对delete[],也可以特化

比如这个例子

shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose());

3.5 shared_ptr的其他函数的使用

先讲use_count,use_count就是返回引用计数的计数为多少而已

shared_ptr<Date> p1(new Date);
shared_ptr<Date> p2(p1);
shared_ptr<Date> p3(p1);
cout << p1.use_count() << endl;

在这里插入图片描述
在这里插入图片描述
注意一下,智能指针的构造是不支持像这样隐式类型转换的

再讲一下make_shared
make_shared的作用就相当于原来的make_pair
在这里插入图片描述
make_shared是一个模版,返回的是是一个shared_ptr
而且具有三个点,说明可以传多个参数

shared_ptr<Date> p = make_shared<Date>(1, 1, 1);

在这里插入图片描述
(1, 1, 1)就是去构造Date传的参数

这个有什么好处呢,如果没有用这个的话
那么Date开辟的空间,和计数(这个也是要开空间的)开辟的空间就不在一起,如果shared_ptr使用太多的话,就会出现内存碎片化的问题(小空间太多了),但是make_shared就是将这两块空间挨着一起开辟的,就不容易出现内存碎片化的问题。

4. shared_ptr的实现

由于前面几个智能指针太简单了,所以我们实现这个就可以了

namespace bit
{
	template<class T>
	class my_ptr
	{
	public:

		//再添加删除器
		/*my_ptr(T* ptr=nullptr)
			:_ptr(ptr)
			,_count(new int(1))
		{
			if (ptr == nullptr)
			{
				*_count = 0;
			}
		}*/
		my_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _count(new int(1))//会自动走初始化列表,没写的也会走初始化列表
		{
			if (ptr == nullptr)
			{
				*_count = 0;
			}
		}

		//要添加删除器的时候,必须还要留一个原来的一个参数的构造函数,因为构造是可以传一个参数和两个参数的(包含删除器)
		//如果总共就写一个构造函数,就是下面这个构造函数,那么就传一个参数的时候,d的类型无法推导,是不会走这个构造函数的,
		//相反还会走拷贝构造
		template<class d>//自己推类型的
		my_ptr(T* ptr, d del)
			:_ptr(ptr)
			,_del(del)
			, _count(new int(1))
		{
			if (ptr == nullptr)
			{
				*_count = 0;
			}
		}

		my_ptr(const my_ptr<T>& ptr)
		{
			_ptr = ptr._ptr;
			_count = ptr._count;
			++*(_count);
		}

		void release()//因为this指针一直传
		{
			--(*_count);
			if ((*_count) == 0)
			{
				_del(_ptr);//会调用_ptr的析构函数
				_ptr = nullptr;
			}
		}

		my_ptr&operator=(const my_ptr& ptr)
		{
			//要先对以前的资源处理
			release();
			if (_ptr != ptr._ptr)
			{
				_ptr = ptr._ptr;
				_count = ptr._count;
				++*(_count);
			}
			return (*this);
		}

		~my_ptr()
		{
			release();
		}

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

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

		int use_count()
		{
			return *_count;
		}

		T* get()//get是获得底层的那个指针
		{
			return _ptr;
		}
		
	private:
		T* _ptr;   
		int* _count;
		//接受删除器
		function<void(T* p)> _del = [](T* p) {delete p; };
	};
}

5. weak_ptr

我们先来看一些程序

class fun
{
public:
	fun()
	{

	}
	~fun()
	{
		cout << "~func()" << endl;
	}
	shared_ptr<fun>* next;
	shared_ptr<fun>* prev;
};
shared_ptr<fun> p1(new fun);
shared_ptr<fun> p2(new fun);

在这里插入图片描述

shared_ptr<fun> p1(new fun);
shared_ptr<fun> p2(new fun);
p1->next = p2;

在这里插入图片描述

shared_ptr<fun> p1(new fun);
shared_ptr<fun> p2(new fun);
p1->next = p2;
p2->prev = p1;

在这里插入图片描述我们可以看出,最后一个程序析构失败了,为什么呢
在这里插入图片描述

第一个正常
在这里插入图片描述
第二个,因为后创建的p2,所以就会先析构
先析构p2
所以p2的计数就会变成1
然后析构p1,析构p1的话,计数变为0,变为0的话就会delete所对应空间
delete还会调用其析构函数,析构成员就是调用成员变量的析构函数,所以就变成了析构_next,那么右边的计数就会变为0,然后再delete右边,就这样两个fun都释放了
像prev这种没有指向对象,是不会去释放的,是不会去打印的
在这里插入图片描述
第三种场景就是相互指向
先析构p2,所以就只有计数变为1
不会delete空间,更不会调用其成员变量的析构函数
在析构p1,也只是计数变为1
然后就结束了
最终计数都没有变为0
最终都没有释放
就这样内存泄漏了
这种场景叫做循环引用
出现这个的原因就是内部的赋值,增加了计数
如果内部的计数不会增加计数的话,就不会内存泄漏
所以我们引用weak_ptr

class fun
{
public:
	fun()
	{

	}
	~fun()
	{
		cout << "~func()" << endl;
	}
	weak_ptr<fun> next;
	weak_ptr<fun> prev;
};

int main()
{
	shared_ptr<fun> p1(new fun);
	shared_ptr<fun> p2(new fun);
	p1->next = p2;
	p2->prev = p1;
	return 0;
}

在这里插入图片描述
weak_ptr可以来用shared_ptr来赋值,而且不会增加计数,其实weak_ptr的底层还是有计数的,是自己单独的计数。但不是shared_ptr的那个计数
在这里插入图片描述
比如你看它还有这个接口呢,主要是看有几个weak_ptr

接下来我们实现weak_ptr
我们这里不实现weak_ptr自己的计数
在这里插入图片描述

	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
		{
			;
		}
		weak_ptr(const my_ptr<T>&ptr)
			:_ptr(ptr.get())
		{}

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

	private:
		T* _ptr;
	};
}
bit::my_ptr<fun> p1(new fun);
bit::my_ptr<fun> p2(new fun);
p1->next = p2;
p2->prev = p1;

在这里插入图片描述

接下来我们在介绍几个weak_ptr的接口
在这里插入图片描述
这个函数主要是看weak_ptr指向的内容是否还有效

weak_ptr<Date> s2;
{
	shared_ptr<Date> s3(new Date);
	s2 = s3;
	cout << s2.expired() << endl;
}
cout << s2.expired() << endl;

在这里插入图片描述
这个就表示里面的时候指向的有效,外面的时候指向的无效,因为s3析构了嘛。weak_ptr根本挡不住
在这里插入图片描述
这个函数的作用就是将weak_ptr指向的shared_ptr作为函数的返回值

shared_ptr<Date> s1;
weak_ptr<Date> s2;
{
	shared_ptr<Date> s3(new Date);
	s2 = s3;
	cout << s2.expired() << endl;
	s1 = s2.lock();
}
cout << s2.expired() << endl;

在这里插入图片描述
将weak_ptr指向的shared_ptr赋值给了s2,所以计数会增加,s2的生命周期还在,那么weak_ptr指向的就有效

6. 内存泄漏

在我们现在这个阶段,就算是内存泄漏了,也没事,因为程序正常结束了的话,就会系统自己去释放
但是如果程序没有正常结束呢,或者这个程序是长期运行的了?
内存泄漏很危险,内存泄漏太多,就会导致卡顿
最危险的是每次都泄漏一点点,那么时间久了就会很卡顿

总结

下一节讲类型转换和特殊类

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值