C++进阶篇8---智能指针

本文探讨了C++中的智能指针,包括RAII原则,以及auto_ptr、unique_ptr和shared_ptr三种类型的原理、使用和线程安全问题。通过实例讲解了如何避免资源泄露和多线程同步问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、引言

为什么需要智能指针?

在上一篇异常中,关于内存释放,我们提到过一个问题---当我们申请资源之后,由于异常的执行,代码可能直接跳过资源的释放语句到达catch,从而造成内存的泄露,对于这种情况,我们当时的解决方案是在抛出异常后,我们先对异常进行捕获,将资源释放,再将异常抛出,但这样做会使得代码变得很冗长,那有没有什么办法能让它自动释放内存资源呢?用智能指针

什么是智能指针?

说到自动释放资源,是不是有点熟悉,我们在学习创建类对象时,就知道当类对象的生命周期结束后,系统会自动调用它的析构函数,完成资源的释放,那么我将指针放入这样一个类对象中,将释放资源的工作交给析构函数,只要该对象生命周期结束,那么就释放该资源,如此就不用在关心资源的释放问题,只要函数栈帧销毁,即该对象被销毁,资源就会自动释放,这就叫智能指针。

智能指针的使用和原理

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

2.具有指针的行为,可以解引用,也可以通过->去访问所指空间中的内容

下面写一个简单的智能指针

namespace zxws
{
	template<class T>
	class smart_ptr
	{
	public:
		smart_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}


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

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

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

但是上面这个智能指针有个严重的问题,一旦有两个对象同时指向同一个资源,那么析构函数就会被调用两次,即资源要被释放两次,会报错,如下


二、库中的智能指针

C++官方给出了3个智能指针

1.auto_ptr

auto_ptr:管理权转移的思想,即一个资源只能有一个指针能对它进行管理,其他的指向这一资源的指针均为空,实现如下

namespace zxws
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

		//管理权限的转移
		auto_ptr(auto_ptr& tmp)
			:_ptr(tmp._ptr)
		{
			tmp._ptr = nullptr;
		}

		auto_ptr& operator=(auto_ptr& tmp)
		{
			if (this != &tmp)//注意自己给自己赋值的情况不需要处理,否则会出问题
			{
				if (_ptr)//释放当前对象中资源
					delete _ptr;

				//管理权限转移
				_ptr = tmp._ptr;
				tmp._ptr = nullptr;
			}
			return *this;
		}


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

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

		T* operator->()
		{
			return _ptr;

		}
	private:
		T* _ptr;
	};
}

 2.unique_ptr

unique_ptr:简单粗暴的防拷贝,即一个指针只能被初始化一次,且只能用不同的资源初始化

实现如下

namespace zxws
{
    template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

		//将拷贝构造和赋值重载直接ban掉
		unique_ptr(const unique_ptr& tmp) = delete;
		unique_ptr& operator=(const unique_ptr& tmp) = delete;

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

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

		T* operator->()
		{
			return _ptr;

		}
	private:
		T* _ptr;
	};
}

3.shared_ptr

shared_ptr是通过引用计数的方式来实现多个shared_ptr对象之间共享资源

具体原理如下

1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
实现如下
namespace zxws
{	
    template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}

		shared_ptr(const shared_ptr& tmp)
			:_ptr(tmp._ptr)
			,_pcount(tmp._pcount)
		{
			(*_pcount)++;
		}

		shared_ptr& operator=(const shared_ptr& tmp)
		{
			//这里注意自己给自己赋值的情况!!!
			//当引用计数为1时,就会出现将资源释放后,在赋值的尴尬情况
			//用this!=&tmp也没用,可能出现两个不同对象指向同一块资源的情况
			//所以用资源的地址来判断最准确
			if (_ptr != tmp._ptr)
			{
				release();
				_ptr = tmp._ptr;
				_pcount = tmp._pcount;
				(*_pcount)++;
			}
			return *this;
		}

		void release()
		{
			if (--(*_pcount)==0)
			{
				delete _ptr;
                delete _pcount;
				_pcount = nullptr;
				_ptr = nullptr;
			}
		}

		~shared_ptr()
		{
			release();
		}

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

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

		T* get() const
		{
			return _ptr;
		}

		int use_count() const
		{
			return *_pcount;
		}

	private:
		T* _ptr;
		int* _pcount;
	};
}

那么引用计数,为什么要用指针开辟的空间,而不是成员变量或者静态成员变量?

1、如果是成员变量,那么每一个shared_ptr对象都会有一个_pcount

2、如果是静态成员变量,那么_pcount将属于一个类

两者都不能满足我们的需求

关于shared_ptr还存在一个循环引用的问题,场景如下

当我们将循环链表的两个结点连接起来的时候,就不会释放结点空间,但是只要有一条边没链接就都能释放,为什么???

而只连接一条边,这个闭环就不复存在,所以两个结点都能释放,那如何解决这种情况?

针对这种情况,C++官方设计出了weak_ptr来和shared_ptr搭配使用,也就是说weak_ptr不增加shared_ptr的引用计数,且不参与资源的释放

实现如下

namespace zxws
{	
    template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& tmp)
			:_ptr(tmp.get())
		{}

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

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

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

	private:
		T* _ptr;
	};
}

(上面三个智能指针的模拟实现是被简化过的,功能不全,但是核心就是这些)

其中auto_ptr这个智能指针基本不用

上面写的三个智能指针还有一个缺陷,就是释放资源的delete写死了,如果我们开的是一个数组,就需要用delete[],否则资源的释放就会出现问题,所以就需要我们定制化它们的释放资源的方式,根据前面的知识,我们可以给它传一个释放资源的仿函数,如下

template<class T>
struct Destroy {
	void operator()(T*_ptr){
		delete[] _ptr;
	}
};
template<class T, class D>
class shared_ptr
{
	//....
};
shared_ptr<int, Destroy<int>>p;

但是库中只写了一个模板参数

我们如果想实现和库中一样的效果,该怎么写?

既然传模板参数不行,我们只能传函数对象了,用function包装器和lambda表达式实现如下

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

		shared_ptr(T* ptr,function<void(T*)> del)
			:_ptr(ptr)
			,_pcount(new int(1))
			,_del(del)
		{}

		shared_ptr(const shared_ptr& tmp)
			:_ptr(tmp._ptr)
			,_pcount(tmp._pcount)
			,_del(tmp._del)
		{
			(*_pcount)++;
		}

		shared_ptr& operator=(const shared_ptr& tmp)
		{
			//这里注意自己给自己赋值的情况!!!
			//当引用计数为1时,就会出现将资源释放后,在赋值的尴尬情况
			//用this!=&tmp也没用,可能出现两个不同对象指向同一块资源的情况
			//所以用资源的地址来判断最准确
			if (_ptr != tmp._ptr)
			{
				release();
				_ptr = tmp._ptr;
				_pcount = tmp._pcount;
				_del = tmp._del;
				(*_pcount)++;
			}
			return *this;
		}

		void release()
		{
			if (--(*_pcount)==0)
			{
				_del(_ptr);
				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}

		~shared_ptr()
		{
			release();
		}

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

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

		T* get() const
		{
			return _ptr;
		}

		int use_count() const
		{
			return *_pcount;
		}

	private:
		T* _ptr;
		int* _pcount;
		function<void(T*)>_del = [](T* ptr) {delete ptr; };
	};
}

其他几个智能指针写法类似,就不写了。

4.shared_ptr的线程安全问题

上面我们模拟实现的shared_ptr其实会有线程安全问题,即当多个线程同时进行拷贝构造时,资源的引用计数就有可能出现错误,导致资源不释放/提前释放,测试代码如下

int main()
{
	zxws::shared_ptr<int> p = new int(10);
	thread t1([&]() {
			for (int i = 0; i < 10000; i++) {
				zxws::shared_ptr<int> tmp(p);
			}
		});

	thread t2([&]() {
			for (int i = 0; i < 10000; i++) {
				zxws::shared_ptr<int> tmp(p);
			}
		});
	t1.join();
	t2.join();
	//cout << p.use_count() << endl;
	return 0;
}

所以我们需要给它锁 / 将引用计数的++操作变为原子操作

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

		shared_ptr(T* ptr,function<void(T*)> del)
			:_ptr(ptr)
			,_pcount(new int(1))
			,_del(del)
		{}

		shared_ptr(const shared_ptr& tmp)
			:_ptr(tmp._ptr)
			,_pcount(tmp._pcount)
			,_del(tmp._del)
		{
			++(*_pcount);
		}

		shared_ptr& operator=(const shared_ptr& tmp)
		{
			//这里注意自己给自己赋值的情况!!!
			//当引用计数为1时,就会出现将资源释放后,在赋值的尴尬情况
			//用this!=&tmp也没用,可能出现两个不同对象指向同一块资源的情况
			//所以用资源的地址来判断最准确
			if (_ptr != tmp._ptr)
			{
				release();
				_ptr = tmp._ptr;
				_pcount = tmp._pcount;
				_del = tmp._del;
				(*_pcount)++;
			}
			return *this;
		}

		void release()
		{
			if (--(*_pcount)==0)
			{
				_del(_ptr);
				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}

		~shared_ptr()
		{
			release();
		}

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

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

		T* get() const
		{
			return _ptr;
		}

		int use_count() const
		{
			return *_pcount;
		}

	private:
		T* _ptr;
		//int* _pcount;
		atomic<int>* _pcount;
		function<void(T*)>_del = [](T* ptr) { delete ptr; };
	};
}

库中的shared_ptr是线程安全的,但是它指向的资源不是,所以当有多线程操作时,需要对资源进行加锁保护

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值