C++11 智能指针


前言: 智能指针,它是指针嘛?它是一个类具有指针的功能,我去,那不是还有一个迭代器嘛,迭代器不就是一个类具有指针的功能。注意这俩可不敢混淆。迭代器是自定义对象的指针,可以这么理解,迭代器的出现使得自定义对象,也可以像内置类型一般进行指针操作。那么智能指针的出现,又有什么意义呢?


1. 智能指针出现的意义

1.1 内存泄漏

内存泄漏可以分为两类:

  1. 堆空间上的内存泄漏:
    堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用。
  2. 系统资源中的内存泄漏 :
    指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

智能指针可以解决内存泄漏的问题,它相对于一种预防手段。因为C++没有回收机制嘛,所以内存泄漏的问题,解决起来十分困难。

比如:你虽然严格按照 new delete ,malloc free 这样写代码。但是 如果程序中途抛异常 有可能就会跳过 你写的 delete 或是 free ,这很难受。造成了内存泄漏。

怎么说呢,内存泄漏是很危险的,尤其是那种长时间运行的程序。一旦出现内存泄漏,会导致程序越来越卡,甚至导致服务器 宕机。所以 程序员再处理内存,指针之类的 都格外小心。有没有一种机制,可以帮助我们 减轻些负担呢?那就是智能指针

1.2 智能指针初识

智能指针利用的是,RALL技术:利用对象的生命周期来控制程序资源。

简单来说:构造类对象,会自动调用构造函数;对象 销毁时,会自动调用对象的析构函数。利用类对象这一特性,就不需要我们手动的释放内存空间。

对于这个大家基本上都懂,但是我也用代码演示一下:

#include<iostream>
using namespace std;

class A
{
private:
	int _a;
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "构造:A()" << endl;
	}

	~A()
	{
		cout << "析构:~A()" << endl;
	}
};

int main()
{
	A a;

	return 0;
}

构造一个A类对象a,我们来看程序运行结果:

在这里插入图片描述

嗯,那么我们来实现一个简易版本的智能指针:

template<class T>
class SmartPtr
{
private:
	T* _ptr;
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
		{
			delete _ptr;
			cout << "delete _ptr" << endl;
		}
	}

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

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

智能指针原理总结:

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

我们来简单的使用一下,上面的智能指针:

    int* iptr = new int(2);
	SmartPtr<int> sptr(iptr);

	*sptr = 10;

来看程序的运行:

在这里插入图片描述

可以看到,是自动释放new 出来的 空间的。


2. C++标准库中的智能指针

C++11之前是有一个:

  • auto_ptr

但由于被喷的惨,所以基本没人用。

C++11 更新后,新给出了三类智能指针:

  1. std::unique_ptr
  2. std::shared_ptr
  3. std::weak_ptr

下面我会一 一 介绍,

2.1 auto_ptr

我们上面不是写过一个简易版本的智能指针,大家可以再看一下。会发现我没有写拷贝构造和赋值重载。其实智能指针,难点就是这俩。我先用上面的简易智能指针去完成一下拷贝,看看会出现什么问题。

	SmartPtr<int> sptr(new int(1));
	SmartPtr<int>sptr1(sptr);

在这里插入图片描述
抛异常了,我们试着,捕获一下:

   try 
	{
		SmartPtr<int> sptr(new int(1));
		SmartPtr<int>sptr1(sptr);
	}

	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

这样不能捕获,因为这个异常抛的是我们自定义类型的,所以不好搞。

我直接说原因吧,sptr 赋值给 sptr1 默认的拷贝构造是 浅拷贝,所以导致同一个块资源被释放了 两次。

怎么解决这个问题呢?有多种方式,就这个问题衍生出的多类的智能指针。

auto_ptr是这样解决的:管理权转移的思想,也就是说,这一块资源我交给要拷贝我的人来管理,我自己呢撒手掌柜,不管了。

这是感性的理解,还是代码实现一下:

    template<class T>
	class auto_ptr
	{
	private:
		T* _ptr;
	public:

		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

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

		auto_ptr(auto_ptr<T>& tmp)
			:_ptr(tmp._ptr)
		{
			tmp._ptr = nullptr;
		}

		auto_ptr& operator=(auto_ptr<T> &tmp)
		{
		    if (_ptr)
			{
				delete _ptr;
			}
			_ptr = tmp._ptr;
			tmp._ptr = nullptr;
			return *this;
		}

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

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

关键是这一步:

在这里插入图片描述

给段代码,通过调试帮助大家理解:

    ly::auto_ptr<int> aptr(new int(1));

	ly::auto_ptr<int> aptr1(aptr);

	ly::auto_ptr<int> aptr2(new int(2));

	aptr2 = aptr1;

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

但是有一个明显的缺陷,那就是拷贝构造,完成了权限转移,直接把我原来管理的资源置为空。虽然我不再管理了,直接把置空了,那么我很难受。我虽然没有权力去释放这块资源,但是 我连访问都成问题了。这样做是不是有点太绝了。

比如:

    ly::auto_ptr<int> aptr(new int(1));

	ly::auto_ptr<int> aptr1(aptr);

	cout << *aptr << endl;

现在就变成了堆空指针的解引用,必然抛异常。所以auto_ptr 这样的做法有点太 一刀两断了。我被别人拷贝了,把管理权交出去了,没有权利去释放资源,但是不能直接把我置空,导致我 无法访问资源。

综上:auto_ptr 用的少,但是 前人踩坑,后人才能避坑。


2.2 std::unique_ptr

怎么说呢,unique_ptr更加暴力,直接就是不允许发生智能指针的拷贝和赋值。呵呵,很强势,当然很简单,我直接实现一下:

   template<class T>
	class unique_ptr
	{
	private:
		T* _ptr;
	public:

		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

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

		unique_ptr(unique_ptr<T>& tmp) = delete;

		unique_ptr& operator=(unique_ptr<T>& tmp) = delete;

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

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

就是将 拷贝构造和赋值重载 delete 关键字修饰一下。

这个好理解对吧。不过多赘述了哈。要提一点就是:

在这里插入图片描述
库里面有第二个模板参数,这个模板参数是指定删除。放后面讲。


2.3 std::shared_ptr

这才是 真正意义上支持拷贝和赋值额 智能指针。利用的是引用计数的思想,也就是说 类中有用于保存 指向此块资源对象的个数,等指向此资源的对象只剩下一个时,如果要析构才会释放资源,其余情况 都是 指向资源个数 减一。

图解:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

简易实现:

我们先来实现一个简易版本的,不考虑线程安全问题,那么想让 所有对象 共用一份 count 计数,有几种方式呢?

我给出两种:

  • 将引用计数设置为静态成员变量
  • 使用指针,引用计数 是堆上 开辟的,所有对象都可以通过指针,来访问同一块堆上空间

那么 简易点就是 第一种方式嘛:

template<class T>
	class shared_ptr
	{
	public:
		static int _count;
		T* _ptr;
	public:
		shared_ptr(T* ptr =nullptr)
			:_ptr(ptr)
		{
			_count = 1;
		}

		~shared_ptr()
		{
			if (--_count == 0 && _ptr)
			{
				delete _ptr;
			}
		}

		shared_ptr(shared_ptr& tmp)
		{
			// 防止自己赋值给自己
			if (_ptr != tmp._ptr)
			{
				_ptr = tmp._ptr;
				_count = tmp._count;
				_count++;
			}
		}

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

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

	};
    template<class T>
	int shared_ptr<T>::_count = 0;

这就是支持拷贝构造,不考虑线程安全的版本,很简单哈。


其实线程安全问题的解决无非就是 加个锁。 多个线程 对 count进行 ++ - - 操作,这里是线程不安全的,所以 对count 操作的地方,都需要加锁。

template<class T>
	class shared_ptr
	{
	private:
		T* _ptr;
		int* _count;
		mutex* _mutex;
	public:
		shared_ptr(T* ptr=nullptr)
			:_ptr(ptr),
			_count(new int(1)),
			_mutex(new mutex) 
		{
		}

		~shared_ptr()
		{
			release();
		}

		shared_ptr(shared_ptr& tmp)
		{
			if (_ptr != tmp._ptr)
			{
				_ptr = tmp._ptr;
				_count = tmp._count;
				_mutex = tmp._mutex;

				tmp.addcount();
			}
		}

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

				_ptr = tmp._ptr;
				_count = tmp._count;
				_mutex = tmp._mutex;

				tmp.addcount();
			}

			return *this;
		}

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

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

		int get_count()
		{
			return *_count;
		}

		void release()
		{
			_mutex->lock();
			bool flag = false;
			if (--(*_count) == 0&&_ptr)
			{
				delete _ptr;
				delete _count;
				flag = true;
			}
			
			_mutex->unlock();

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

		void addcount()
		{
			_mutex->lock();
			(*_count)++;
			_mutex->unlock();
		}
	};

测试代码:

        shared_ptr<int> sptr(new int(1));
	    shared_ptr<int> sptr1(sptr);
	    shared_ptr<int> sptr2(new int(2));
     	sptr2 = sptr1;

shared_ptr 其实还有一个问题需要格外注意循环引用。循环引用会导致,本该释放的资源,得不到释放,也就是说 count 加多了,这解释有点牵强,大家一会看图理解:

首先给出一个例子,链表的节点:

在这里插入图片描述
节点里面的指针,我们可以用智能指针嘛?试着用一下,因为 智能指针 还是有好处的,它可以预防 内存泄漏 对吧,但是这里会出现 循环引用的问题:

struct  Node
{
	int val;
	ly::shared_ptr<Node> _next;
	ly::shared_ptr<Node> _prev;

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

假如我这样使用节点:

    shared_ptr<Node> n1(new Node);
	shared_ptr<Node> n2(new Node);
	cout<<n1.get_count()<<endl;
	cout << n2.get_count() << endl;

看结果,是对的:

在这里插入图片描述

假如我让它俩互相指向呢?

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

	cout<<n1.get_count()<<endl;
	cout << n2.get_count() << endl;

	n1->_next = n2;
	n2->_prev = n1;
	cout << n1.get_count() << endl;
	cout << n2.get_count() << endl;

在这里插入图片描述

发现尽然没有析构,没释放资源。程序都退出了,这就是内存泄漏。

造成这个的原因,我们来分析一下:

刚开始 没问题:

在这里插入图片描述
但是由于n1->_next = n2; n2->_prev = n1; 所以 count ++了:

在这里插入图片描述
如果进行析构,那么 就是 count – ,它减完之后,变为1 ,所以 不会进行 资源释放。

其实问题已经分析出来了,count 如果不 ++ 那么 资源还能够释放,因为互相指向,所以它俩的count 都 ++了。那么有没有解决办法呢?那就是 std::weak_ptr ,它呢,就是 不参与 资源管理,虽然指向了某块资源,但是 count 不会 ++。


2.4 std::weak_ptr

weak_ptr 其实就是 专门用于 解决 shared_ptr 中循环指向的问题的。

废话不多说,直接就是 将节点中的 shared_ptr 换成 weak_ptr 就可以了:

在这呢 先使用 标准库中的 shared_ptr 和 weak_ptr ,之后再模拟实现weak_ptr,

struct  Node
{
	int val;
	std::weak_ptr<Node> _next;
	std::weak_ptr<Node> _prev;

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

int main()
{
    std::shared_ptr<Node> n1(new Node);
	std::shared_ptr<Node> n2(new Node);

	cout<<n1.use_count()<<endl;
	cout << n2.use_count() << endl;

	n1->_next = n2;
	n2->_prev = n1;
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

 return 0;
}

看运行结果,很明显完成了资源释放:

在这里插入图片描述
而且 发现 count的值 没有变成 2,原因很简单 weak_ptr 智能指针不参与 资源管理。

那么 我们来模拟实现一下,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;
		}


	private:
		T* _ptr;
	};

对吧,就是不让它参与资源管理就行,唯一需要注意的就是它的拷贝构造,可以是shared_ptr 也可以是 weak_ptr 。

在这里插入图片描述


3. 智能指针中的定制删除

像上面所有的智能指针模拟实现都是 new 和delete ;但是 还有别的情况 比如 delete [] , free ,fclose() 。对吧,所以呢 智能指针 提供了 定制删除,默认情况下是 delete 。

在这里插入图片描述

看 默认参数D 是 default_delete< T >:

在这里插入图片描述
看到了吧,这是啥?仿函数呀,昂,可以 。我们一会 来模拟实现一下 free 版本的

但是 shared_ptr 中 定制删除 不是给的模板参数,而是 在构造函数重载中的一个:

在这里插入图片描述

所以定制删除在智能指针中的使用,要自己去标准库中查看,但定制删除一般都是是仿函数。区别就是 模板参数 给的是类型,构造函数中 给的是 对象。

但是 定制删除 如果是在 sharde_ptr中给的不就是个对象嘛,所以也可以给 lambda表达式。


所以 我们先来 给出 两个定制删除器:

  1. delete []
template<class T>
struct DeleteArray
{
	void operator()(const T* ptr)
	{
		delete[] ptr;
	}
};
  1. fclose()
struct DeleteFile
{
	void operator()(FILE* ptr)
	{
		fclose(ptr);
	}
};

使用起来也很简单:

std::unique_ptr<A, DeleteArray<A>> up2(new A[10]);
std::unique_ptr<FILE, DeleteFile> up3(fopen("test.txt", "w"));
std::shared_ptr<A> sp2(new A[10], DeleteArray<A>());
std::shared_ptr<FILE> sp3(fopen("test.txt", "w"), DeleteFile());

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

动名词

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值