智慧指针底层实现及其使用注意

本文详细探讨了C++中的智能指针(如std::unique_ptr,std::shared_ptr,和weak_ptr)的工作原理,重点讲解了它们如何通过RAII和引用计数机制管理资源,以及在异常处理和循环引用场景中的应用。文章还提到了如何避免delete[]引发的问题,并讨论了weak_ptr在解决循环引用中的角色以及线程安全问题。
摘要由CSDN通过智能技术生成

为什么要有智慧指针?
当抛出异常时,程序会立即跳转到匹配的 catch 块,跳过 try 块中剩余的代码。因此,在 try 块中分配的资源(如动态内存、文件句柄等)可能不会被正确释放。为了避免这种情况,可以使用智能指针(如 std::unique_ptr 和 std::shared_ptr)和 RAII(Resource Acquisition Is Initialization)技术来确保资源在异常发生时得到正确清理。

智能指针 说白了 就是 指针管理
那他是怎么管理的呢?你可以先看看smart_ptr

RAII
资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源
1、RAII管控资源释放
2、像指针一样
3、拷贝问题

smart_ptr

template<class T>
class SmartPtr
{
public:
	// RAII
	// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源
	// 1、RAII管控资源释放
	// 2、像指针一样
	// 3、拷贝问题
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	~SmartPtr()
	{
		cout << "delete: " << _ptr << endl;
		delete _ptr;
	}
private:
	T* _ptr;
};

smart_ptr利用RAII技术在生命周期内管理了指针,但是如果发生智能指针的之间的拷贝就会导致析构两次的错误,而且指针之间要的就是浅拷贝因为普通指针就是浅拷贝。

int main()
{
	SmartPtr<string> p1(new string("xxx"));
	SmartPtr<string> p2(new string("111"));
	p1 = p2;
	//默认浅拷贝,析构两次p2
	// 
	//p1的空间没有释放,内存泄露

	//智慧指针模拟的是普通指针,这里的赋值要的就是浅拷贝

	return 0;
}

下面我们来看看C++里提供的各种智能指针

auto_ptr

template <class T>
	class auto_ptr
	{
	public:
		//RAII
		//像指针一样
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		// ap3(ap1)
		// 管理权转移
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
	
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		~auto_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	private:
		T* _ptr;
	};

管理权转移导致的指针为空问题

int main()
{
	// C++98 一般实践中,很多公司明确规定不要用这个
	bit::auto_ptr<A> ap1(new A(1));
	bit::auto_ptr<A> ap2(new A(2));

	// 管理权转移,拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象
	// 隐患:导致被拷贝对象悬空,访问就会出问题
	bit::auto_ptr<A> ap3(ap1);
	
	//ap1->_a++;// 崩溃
	ap3->_a++;

	return 0;
}

unique_ptr

简单粗暴,不让拷贝

template<class T>
	class unique_ptr
	{
	public:
		//RAII
		//像指针一样
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
		
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		// ap3(ap1)
		//防拷贝
		unique_ptr(const unique_ptr<T>& up) = delete;

		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
	private:
		T* _ptr;
	};

可指针不让我拷贝也不行啊,所以就有了shared_ptr

shared_ptr

利用一个引用计数来共同管理某一个资源,那这个计数器如何设计?
用一个类内成员int _count行不行?
在这里插入图片描述

不行,每个智能指针对象的计数各自是各自的计数,个玩的个,我们期望指针都要访问一个独立的计数

用一个类内的静态Int变量行不行?
不行,类内静态变量属于这个类,也属于这个类的所有对象,这种情况就需要两个计数,那你静态只有一个,所以不行
在这里插入图片描述
我们期望一个资源伴随一个计数,于是这个计数就是new出来的int* count ,则一个指针指向资源,一个指针指向引用计数。
在这里插入图片描述
每多一个拷贝或赋值的智慧指针指向同一个资源,引用计数就加加,析构先减减计数,等计数为0就delete资源,和delete引用计数

那如何delete[ ] arr 呢?而且new 和 delete 一定要匹配,如何解决?
在这里插入图片描述

拷贝构造挺简单的,因为语法上限制了自己给自己拷贝构造,赋值也是一个硬茬,因为可以自己给自己赋值,sp1 = sp1;
又或者sp1 = sp5会发生啥?
sp1指向的资源的引用计数要减减,涉及到减减计数就要考虑是否计数是1,如果是就要析构资源,而后再让sp5指向的资源计数++;

在这里插入图片描述
如果是自己给自己赋值,那么就判断我资源指针和你的资源指针是否是同一个,如果是判断后直接返回。
在这里插入图片描述

如果不判断那么自己给自己赋值的话,上来就把资源释放,最后又赋值给自己,那么就是野指针了,资源指针和计数指针都是随机值了。
在这里插入图片描述

在这里插入图片描述

还会有这种间接的自己给自己赋值 ,直接判断是资源同一个指针后直接返回
在这里插入图片描述

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

		template<class D>
		shared_ptr(T* ptr, D del)//shared_ptr(T* ptr = nullptr,D del)//传缺省参数时只能从右往左给缺省
			:_ptr(ptr)
			,_pcount(new int(1))
			, _del(del)
		{}

		~shared_ptr()
		{
			if ((--(*_pcount)) == 0)
			{
				cout << "delete:" << _ptr << endl;
				_del(_ptr);
				delete _pcount;
			}
		}
		//sp3(sp1)
		shared_ptr(const shared_ptr<T>& up)
			:_ptr(up._ptr)
			,_pcount(up._pcount)
		{
			++(*_pcount);
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& up)
		{
			//处理自己给自己赋值导致野指针
			//1.s1 = s1
			//2.s4 = s5  此种情况也是自己给自己赋值
			if (_ptr == up._ptr)
				return *this;

			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
			_ptr = up._ptr;
			_pcount = up._pcount;
			++(*_pcount);

			return *this;
		}
		
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		int use_count() const 
		{
			return *_pcount;
		}
		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
															//
		function<void(T*)> _del = [](T* ptr) {delete ptr; };//删除器设置默认参数,默认智能指针用delete释放
	};

直接用指针构造的shared_ptr的风险

int main()
{
	int* a = new int();
	ljh::shared_ptr<int> a1(a);
	ljh::shared_ptr<int> a2(a);

	
	return 0;
}

他们两个指针指向同一个资源,但是直接构造,会在构造函数里开辟两个引用计数,都是1,造成double free
在这里插入图片描述
在这里插入图片描述

shared_ptr循环引用

是什么?

我对象成员里面有一个智能指针指向你,你对象成员里面有一个智能指针指向我
现象
循环引用的现象也会造成double free,或者是给你看到资源没释放也就是内存泄漏
在这里插入图片描述

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

	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	//private:

	int _a;
};

struct Node
{
	A _val;

	/*ljh::weak_ptr<Node> _next;
	ljh::weak_ptr<Node> _prev;*/

	shared_ptr<Node> _next;
	shared_ptr<Node> _prev;
};
int main()
{
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);

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

	sp1->_next = sp2;
	sp2->_prev = sp1;

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
	return 0;
}

上面的代码在没有执行sp1->_next = sp2; sp2->_prev = sp1; 也就是让他们互相指向时,都没事,但当他们互相指向了以后就有问题了
在这里插入图片描述

为什么?
前置知识:大对象先析构,小成员后析构

B里面包含了A对象,但是是B先调用析构函数,然后A a再调用自己的析构,就是这么一个顺序
在这里插入图片描述
如果B里面有多个成员呢?
和成员变量在类中声明次序有关1 A 2 C,他们又是在栈上开辟的,所以后进先出 。
在这里插入图片描述
有了这个前置知识以后再看,两个对象出了main作用域,让其双方引用计数都减到1,然后就没有然后了,VS调试到最后其实就结束了。
在这里插入图片描述
sp1和sp2不再指向节点,只剩next和prev指向节点
但是如果此时某一个节点,比如说右边节点要析构,你调用了删除erase,根据前置知识,_prev就会析构,prev是shared_ptr,它调用析构就让左边节点引用计数减减到0,左边节点就析构了,又根据前置知识,则左边节点中成员next也要析构,就会又让右边节点析构,右边节点析构就又让成员prev析构,也就是又让左边节点析构。此时double free。
一句话就是你析构节点就double free,不析构那就内存泄漏。

又或者你用图上的逻辑来看,它会一直循环着你释放我我释放你。

怎么办?

weak_ptr就上场了,但注意 weak_ptr不是RAII智能指针,它专门用来解决shared_ptr循环引用问题
weak_ptr它解决的原理就是weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理
在memory库里 甚至 weak_ptr 都没有重载 —>运算符

既然不参与管理资源增加引用计数,那么左边右边的计数都还是1,出作用域就释放,那就没问题。
在这里插入图片描述
我们要解决循环引用,第一步先得认识这个场景会产生循环引用的问题,比如你里面有个share_ptr指向我,我里面有一个share_ptr指向你,甚至是我这个对象里成员结构中里保存有的智能指针指向你。然后我们才能利用weak_ptr解决它。

wake_ptr

wake_ptr 不支持用一个普通指针来构造成智能指针,他只支持 wake_ptr ptr默认构造 or 用一个shared_ptr类型智能指针来拷贝构造 或 赋值,因为它不参与资源释放的管理,只是单纯访问资源。
所以一开始如果不了解底层,就会想那既然wake_ptr这么牛,能解决解决shared_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;
	};

库里的weak_ptr

定制删除器,解决智慧指针delete [ ]无法适配的问题

前置知识

lambda 是一个可调用对象,可调用对象分为四种,函数指针,仿函数,lambda,包装器

包装器可以把它当成函数指针
在这里插入图片描述
库里面是通过构造函数传入一个可调用对象来完成各种delete ; free; 甚至是文件关闭。
在这里插入图片描述
在这里插入图片描述
只有shared_ptr支持了定制化删除器,删除数组资源等,其他智能指针没支持。
在这里插入图片描述

库里面构造时给了两个模板参数,我们就用一个就可以了,因为本身shared_ptr类就有T模板可以给指针类型。
因为他是在构造函数里传入的定制删除器,并不是在类模板参数里面传入的,
我们要控制析构函数里的删除方式,用这个可调用对象删除,那析构函数怎么使用删除器的类型D呢?
你可以用一个成员变量_del保存你传入的删除器,但是因为外部没有D模板参数,D是构造函数的模板参数,不是类模板的模板参数,所以我不知道D的类型是什么,没法定义_del成员。
在这里插入图片描述

你可以给类模板增加参数,但是这和库实现不一致,所以我们采用第二种优雅的方法。
我们可以用包装器将可调用对象类型包装起来,也就是函数类型是确定的,函数参数都是shared_ptr的模板参数T*,
返回值都是void,因为不管你是new,还是malloc,或者文件,你释放时都是给函数传入T类型指针,函数返回值是空。

在这里插入图片描述
到这里解决了删除数组或者malloc等情况,但是默认指针又没法删除了,可以给_del默认初始化一个lambda对象,
让它默认就是delete ptr就可以了。
在这里插入图片描述
不要忘记赋值重载里也涉及计数减减和析构问题,所以它默认delete ptr 也要改为用删除器删除。
在这里插入图片描述

智慧指针线程安全问题

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值