前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【C++】智能指针

【C++】智能指针

作者头像
青衫哥
发布2023-10-17 08:43:25
1950
发布2023-10-17 08:43:25
举报
文章被收录于专栏:C++打怪之路C++打怪之路

一、为什么需要智能指针?

在我们异常一节就已经讲过,当使用异常的时候,几个函数层层嵌套,其中如果抛异常就可能导致没有释放堆区开辟的空间。这样就很容易导致内存泄漏。关于内存泄漏,我也曾在C++内存管理一文中写过。

为了更好的管理我们申请的空间,C++引入了智能指针。

参考文章:

1.【C++】异常_

2. C++内存管理

二、智能指针

1.RAII

RAII ( Resource Acquisition Is Initialization )是一种 利用对象生命周期来控制程序资源 (如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在 对象析构的时候释放资源 。借此,我们实际上把管理一份资源的责任托管给了一个对象。

这种做 法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
代码语言:javascript
复制
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{
		cout << "管理空间:" << _ptr << endl;
	}

	~SmartPtr()
	{
		if (_ptr)
		{
			delete _ptr;
		}
		cout << "释放空间:" << _ptr << endl;
	}

	
private:
	T* _ptr;
};

int main()
{
	SmartPtr<int> sp1(new int(1));

	return 0;
}

我们可以看到,智能指针的引入,极大的便利了我们管理空间。

在封装了几层的函数中抛异常,我们也能够来通过智能指针来管理好空间。

2.智能指针的完善

上述的 SmartPtr 还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过-> 去访问所指空间中的内容,因此: AutoPtr 模板类中还得需要将 * -> 重载下,才可让其 像指针一样去使用

代码语言:javascript
复制
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{
		cout << "管理空间" << _ptr << endl;
	}

	~SmartPtr()
	{
		if (_ptr)
		{
			delete _ptr;
		}
		cout << "释放空间:" << _ptr << endl;
	}

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

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

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

private:
	T* _ptr;
};

class Date
{
public:
	int _year;
	int _month;
	int _day;
};


int main()
{
	SmartPtr<int> sp1(new int(1));
	cout << *sp1 << endl;

	SmartPtr<int> sp2(new int[10]);
	sp2[2] = 10;
	cout << sp2[2] << endl;

	return 0;
}

通过符号的重载,我们使得智能指针具有了普通指针的功能。

但是我们发现,智能指针没有提供拷贝的功能,那么接下来我们看看库中实现的智能指针是如何做的?


三、标准库中的智能指针

1.std::auto_ptr

参考文献:std::auto_ptr

auto_ptr 是C++库中的第一个智能指针,其在面对拷贝构造的解决办法是:转移所有权(当用当前的智能指针对象拷贝出一个新对象时,当前对象资源的所有权会转移给新对象,然后自身的资源会置空)。这样也随之带来一个问题,新对象产生的同时,旧对象将会导致对象悬空。如果后续还有人使用了旧对象,就会引发问题。

代码语言:javascript
复制
int main()
{
	std::auto_ptr<int> ap1(new int(10));
	std::auto_ptr<int> ap2(ap1);

	cout << *ap1 << endl;
	return 0;
}

 库中实现方法模拟:

代码语言:javascript
复制
template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			// 管理权转移
			sp._ptr = nullptr;
		}
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 检测是否为自己给自己赋值
			if (this != &ap)
			{
				// 释放当前对象中资源
				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}
		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

2.std::unique_ptr

参考文献: std::unique_ptr

unique_ptr 的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份 UniquePtr 来了解它的原 理 。

代码语言:javascript
复制
//用delete关键字来不让拷贝构造函数生成
unique_ptr(unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;

通过使用delete关键字,将拷贝构造函数删除,使得其无法生成,来实现无法拷贝的操作。

3.shared_ptr

A)shared_ptr中的引用计数 

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

1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。 2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。 3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源; 4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

那么代码中该如何实现呢?有以下几种方法:

1.在成员变量中增加了一个整数类型来记录 。因为每个对象都会有一个自己的成员变量,我们修改的时候需要照顾到每一个指向同一块空间的智能指针对象,这样的办法是不可行的。

2.在类中增加了一个静态的整数类型成员变量。这样就变成了整个类共享这一个成员变量,所以这个办法也是不可行的。

3.添加一个int类型的指针,int类型中记录的是指向其的指针的个数。库中采用的也是这种办法。

实现代码:

代码语言:javascript
复制
template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T*  ptr = nullptr)
			:_ptr(ptr)
			,_count(new int(1)) 
		{}

		~shared_ptr()
		{
			release();
		}

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

		//拷贝构造函数
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_count(sp._count)
		{
			(*_count)++;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//不能给自己赋值
			if (_ptr != sp._ptr)
			{
				//先把原先的资源释放一次
				release();

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

			return *this;
		}

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

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

		T operator[](size_t pos) const
		{
			return _ptr[pos];
		}

		int use_count() const 
		{
			return *_count;
		}

		T* get() const
		{
			return _ptr;
		}


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

我们通过下面函数测试:

代码语言:javascript
复制
void TestShared_ptr()
{
	shared_ptr<int> sp1(new int(10));
	shared_ptr<int> sp2(sp1);
	shared_ptr<int> sp3(new int(5));
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
	cout << sp1.get() << endl;
	cout << sp2.get() << endl;
	sp1 = sp3;
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
	cout << sp1.get() << endl;
	cout << sp2.get() << endl;

}
B)shared_ptr中产生的线程安全问题

我们代码有时候可能是一行,但是转成汇编之后可能有几行,其中++和--操作就是这样。在++操作转成汇编之后,有三行操作,如果在这时候时间片轮转到了时间,将正在运行的线程切出,别的线程也对其中的数据进行操作的时候,就会引发问题了。这是因为这样的操作是非原子性的。

我们用下面的代码来验证:

代码语言:javascript
复制
void TestShared_ptr2()
{
	shared_ptr<int> sp1(new int);
	int N = 10000;

	thread t1([&]() //lambda表达式
		{
			for (int i = 0; i < N; ++i)
			{
				shared_ptr<int> sp2(sp1);
			}
		});

	thread t2([&]() //lambda表达式
		{
			for (int i = 0; i < N; ++i)
			{
				shared_ptr<int> sp3(sp1);
			}
		});

	t1.join();
	t2.join();

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

出现的答案有很多种,可能是整数、负数,甚至还可能会报错。

为了解决这个问题,我们需要给++操作来加锁,使得该操作具有原子性(通俗理解为:要么不做,要么就做完)。

因为枷锁和解锁的过程是具有原子性的,所以,不需要我们担心锁的安全问题。

和引用计数的实现方法一样,我们加锁的操作也是在成员变量中增加一个锁类型的指针。

代码语言:javascript
复制
template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T*  ptr = nullptr)
			:_ptr(ptr)
			,_count(new int(1))
			,_mtx(new mutex)
		{}

		~shared_ptr()
		{
			release();
		}

		void release()
		{
			bool flag = false;
			_mtx->lock();
			if (--(*_count) == 0)
			{
				delete _ptr;
				delete _count;

				flag = true;
			}

			_mtx->unlock();

			//如果清空了数据,那么锁也要释放
			if (flag == true)
			{
				delete _mtx;
			}
		}

		//拷贝构造函数
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_count(sp._count)
			,_mtx(sp._mtx)
		{
			_mtx->lock();
			(*_count)++;
			_mtx->unlock();

		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//不能给自己赋值
			if (_ptr != sp._ptr)
			{
				//先把原先的资源释放一次
				release();

				_ptr = sp._ptr;
				_count = sp._count;
				_mtx->lock();
				(*_count)++;
				_mtx->unlock();
			}

			return *this;
		}

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

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

		T operator[](size_t pos) const
		{
			return _ptr[pos];
		}

		int use_count() const 
		{
			return *_count;
		}

		T* get() const
		{
			return _ptr;
		}


	private:
		T* _ptr;
		int* _count;
		mutex* _mtx;
	};

值得一提的是,虽然引用计数我们在类内加锁了,但是如果在线程中对智能指针中的资源++的时候,还是不安全的。

代码语言:javascript
复制
void TestShared_ptr2()
	{
		shared_ptr<int> sp1(new int);
		int N = 10000;

		thread t1([&]() //lambda表达式
		{
				for (int i = 0; i < N; ++i)
				{
					++(*sp1);
				}
		});

		thread t2([&]() //lambda表达式
		{
				for (int i = 0; i < N; ++i)
				{
					++(*sp1);
				}
		});

		t1.join();
		t2.join();

		cout << *sp1 << endl;
	}

库中提供的shared_ptr也是有这个问题的。所以在我们对智能指针中的资源操作的时候,我们也需要手动加锁。

C)shared_ptr中的循环引用问题

虽然shared_ptr相较于以往的智能指针,表现的十分好,但是仍旧是有缺陷的。

我们看下面的问题:

代码语言:javascript
复制
struct ListNode
	{
		shared_ptr<ListNode> _prev;
		shared_ptr<ListNode> _next;
	};
void TestShared_ptr3()
	{
		shared_ptr<ListNode> sp1(new ListNode);
		shared_ptr<ListNode> sp2(new ListNode);

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

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

  当我们存储的是双向链表的节点并且剩下两个节点的时候,这时候sp1中存储的链表节点n1的_prev指向sp2中,n2的_next指向的是n1节点。当代码结束后,调用析构函数,此时的_count == 3,调用析构--count,此时仍旧不等于0,而如果要满足0释放空间,则需要:

1.sp1需要释放,则需要n2先释放,n2中的指针清除之后,_count == 0,才能释放。

2.sp2需要释放,需要n1先释放,n1中的指针清除之后,_count == 0,才能完成释放。

所以,我们会发现,这就造成死循环了。就像两个小孩打架抓着对方的头发,A对B说你先放手我就放,B也对A说你先放手我就放。

为了解决这个问题,C++中引入了weak_ptr。

4.weak_ptr

weak_ptr是为了解决shared_ptr而专门设计出的一款智能指针,解决办法也很简单,那就是不设计引用计数,自然也就不会有因为 count  != 0 而无法释放空间的问题了。

由于库中的weak_ptr考虑的非常全面,我们这里只对解决shared_ptr的缺陷作模拟。

代码如下:

代码语言:javascript
复制
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*() const
	{
		return *_ptr;
	}

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

private:
	T* _ptr;
};
struct ListNode
{
	qingshan::weak_ptr<ListNode> _prev;
	qingshan::weak_ptr<ListNode> _next;
};
void TestShared_ptr3()
{
	qingshan::shared_ptr<ListNode> sp1(new ListNode);
	qingshan::shared_ptr<ListNode> sp2(new ListNode);

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

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

最后我们也是看到,节点成功释放了。

5.定制删除器

我们在析构函数中只用了delete来释放申请的空间,那么如果我们使用new[ ] 来申请的空间,那么同样的我们也需要用 delete[ ] 来释放空间。

为此,我们就需要定制删除器。定制删除器本质上是一个仿函数。与我们在哈希一文中提到的hashfunc一样。

我们还需要再shared_ptr类中增加一个成员变量 _del 来实现释放空间。

代码如下:

代码语言:javascript
复制
template<class T, class D = default_delete<T>>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _count(new int(1))
		, _mtx(new mutex)
	{}

	~shared_ptr()
	{
		release();
		cout << "~shared_ptr" << endl;
	}

	void release()
	{
		bool flag = false;
		_mtx->lock();
		if (--(*_count) == 0)
		{
			//delete _ptr;
			_del(_ptr);

			delete _count;

			flag = true;
		}

		_mtx->unlock();

		//如果清空了数据,那么锁也要释放
		if (flag == true)
		{
			delete _mtx;
		}
	}

	//拷贝构造函数
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _count(sp._count)
		, _mtx(sp._mtx)
	{
		_mtx->lock();
		(*_count)++;
		_mtx->unlock();

	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//不能给自己赋值
		if (_ptr != sp._ptr)
		{
			//先把原先的资源释放一次
			release();

			_ptr = sp._ptr;
			_count = sp._count;
			_mtx->lock();
			(*_count)++;
			_mtx->unlock();
		}

		return *this;
	}

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

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

	T operator[](size_t pos) const
	{
		return _ptr[pos];
	}

	int use_count() const
	{
		return *_count;
	}

	T* get() const
	{
		return _ptr;
	}


private:
	T* _ptr;
	int* _count;
	mutex* _mtx;

	D _del;
};


void TestShared_ptr4()
{
	qingshan::shared_ptr<int> sp1(new int[10]);
	qingshan::shared_ptr<int, DeleteArray<int>> sp2(new int[10]);
	qingshan::shared_ptr<FILE, Fclose> sp3(fopen("test.txt", "w"));

}

定制删除器功能十分强大,我们甚至还可以用其来关闭文件。但是我们这里实现的只能在模版中提供类型来定制删除器。

库中的提供的shared_ptr是能够通过给构造函数传参来定制删除器的,所以还能使用包装器和lambda表达式。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-10-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么需要智能指针?
  • 二、智能指针
    • 1.RAII
      • 2.智能指针的完善
      • 三、标准库中的智能指针
        • 1.std::auto_ptr
          • 2.std::unique_ptr
            • 3.shared_ptr
              • A)shared_ptr中的引用计数 
              • B)shared_ptr中产生的线程安全问题
              • C)shared_ptr中的循环引用问题
            • 4.weak_ptr
              • 5.定制删除器
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档