c++11智能指针

一、内存泄漏

在前文中我们提到由于c++没有垃圾回收器,内存泄漏问题会用智能指针解决,不妨先看一个没有智能指针防止内存泄漏的例子。

double Division(int a, int b)
{
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void Func()
{
	int* p1 = new int[10];
	int* p2 = nullptr;
	//p2 new的过程中会抛异常
	try
	{
		p2 = new int[20];
		//函数Division会有除0错误抛异常
		try
		{
			int len, time;
			cin >> len >> time;
			cout << Division(len, time) << endl; // throw
		}
		//捕获除0错误 要释放p1 p2
		catch (...)
		{
			delete[] p1;
			cout << "delete:" << p1 << endl;

			delete[] p2;
			cout << "delete:" << p2 << endl;

			throw;  // 捕获什么抛出什么
		}
	}
	//除了除0错误 其他错误捕获 释放p1并抛异常
	catch (...)
	{
		delete[] p1;
		cout << "delete:" << p1 << endl;

		throw;
	}
	//函数没问题 依然释放p1 p2
	delete[] p1;
	cout << "delete:" << p1 << endl;

	delete[] p2;
	cout << "delete:" << p2 << endl;
}

会发现函数有时候还未执行完就会出现各种状况,使函数无法正常执行完毕,导致申请的空间不会因为函数的正常返回释放资源,就会导致内存泄露。

这时候每 try 一次,在 catch 里面就要手动释放一次资源,一旦申请资源过多,手动释放就会极其麻烦。

二、智能指针原理

原理一:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

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

原理二:重载operator*和opertaor->,具有像指针一样的行为。

对象在出了作用域时就会调用析构函数,利用这一特性,我们可以封装一个指针类,构造时保存指针,析构时释放指针指向的资源。做到自动等对象销毁调用析构函数,从而防止内存泄漏。

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

	T* GetPtr()
	{
		return _ptr;
	}

	~SmartPtr()
	{
		delete[] _ptr;
		cout << "delete: " << _ptr << endl;
	}

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

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

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

private:
	T* _ptr;
};
//智能指针版
void Func()
{
	SmartPtr<int> sp1(new int[10]);
	//如果sp2 new抛异常 sp1自动释放
	SmartPtr<int> sp2(new int[20]);

	//如果Division函数抛异常,跳到catch地方
	//出作用域指针sp1 sp2自动释放
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}

这样即使遇到除0错误会跳转到 catch 的地方也不用担心内存没有释放的问题。

三、智能指针的种类

1、c++98 auto_ptr

非常不推荐使用。当使用拷贝构造时会导致原来的指针失去对原有资源的管理权,导致悬空。很像移动构造,但是将亡值可以随意移动资源,对于这里的左值指针是万万不能随意转移管理权的。

最后想打印 sp1 指向的资源就会报错。

2、c++11 unique_ptr

不支持拷贝构造。

get():获取指针

operator bool:函数重载,若指针为空返回false

由于不支持拷贝构造,指向一份资源的只有一个指针,所以是 unique,但是很多时候需要多个指针指向同一块资源,所以引入 shared_ptr

3、c++11 shared_ptr

底层使用引用计数来实现多指针指向同一资源。

由于多指针指向同一份资源,资源只能释放一次,规定指针销毁引用计数-1,减到0调用析构函数(准确说是删除器)。

内存级示意图:

所以 make_shared 构造的对象可以减少内存碎片。

4、c++11 weak_ptr

辅助 shared_ptr 解决循环引用问题。文章后面详解。

四、模拟实现 shared_ptr

1、定制删除器

由于 shared_ptr 底层默认调用 delete 来删除指针指向的资源。

但是如果今天我用的是 FILE* 的文件指针,用 delete 就不能完成删除资源的工作。

所以我们需要了解定制删除器(本质就是函数指针,仿函数,lambda表达式)。

上图中构造函数中的 D del 就是定义删除器。

2、代码实现

#pragma once
#include<atomic>
#include<functional>
namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			//构造时new一块空间存引用计数,初始化成1
			, _pcount(new atomic<int>(1))
		{}

		template<class D>
		shared_ptr(T* ptr, D del)
			:_ptr(ptr)
			//构造时new一块空间存引用计数,初始化成1
			, _pcount(new atomic<int>(1))
			,_del(del)
		{}


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

		int use_count()
		{
			return *_pcount;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//直接判断资源是否相同,避免自己给自己赋值
			if (_ptr != sp._ptr)
			{
				//空间不是你this一个人的,你要修改先判断引用计数
				//如果没有,引用计数不会到0,内存泄漏
				release();

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

			return *this;
		}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				//最后一个管理对象释放资源
				_del(_ptr);
				delete _pcount;
			}
		}

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

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




		~shared_ptr()
		{
			//if (--(*_pcount) == 0)
			//{
			//	//最后一个管理对象释放资源
			//	delete _ptr;
			//	delete _pcount;
			//}
			release():
		}
	private:
		T* _ptr;
		atomic<int*> _pcount;
		//包装删除器
		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};
}

3、解读代码

(1)构造函数

构造时 new 一块空间存引用计数,初始化成1,这样之后拷贝的对象就能看到同一个引用计数。

(2)拷贝构造

初始化 + _pcount个数加1

(3)release函数

如果引用计数减到0就释放 _ptr _pcount

(4)赋值重载

先判断指向的资源是否相同,不同才能赋值 + _pcount个数加1

(5)析构函数

调用 release 函数

(6)私有成员

由于 ++ -- 操作不是原子的,为了保证线程安全,可以加锁,也可以直接定义 atomic 保证 _pcount 原子性。

定义一个返回值 void 参数 T* 的删除器,用 function 包装,默认用 delete 删除。

4、包装删除器用法

class A
{
public:
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
	int _a2 = 2;
};

int main()
{
	shared_ptr<A> sp1((A*)malloc(sizeof A), [](A* ptr) {free(ptr); });
	shared_ptr<A> sp2(new A[10], [](A* ptr) {delete[] ptr; });
	shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });

	return 0;
}

最常用定义删除器就是 lambda 表达式

五、shared_ptr 循环引用问题

1、问题介绍

为什么没有调用到析构函数?

2、图解

3、c++11 weak_ptr

构造函数中有一个用 shared_ptr 构造 weak_ptr,weak_ptr 不支持RAII,不单独管理资源,辅助 shared_ptr 解决循环引用问题。

解决原理

weak_ptr 赋值,拷贝时指向资源,但不增加 shared_ptr 引用计数。

看到调用了析构函数。

expired 判断指针是否过期,本质看引用计数是否为0,防止引用计数为0时,weak_ptr 访问资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值