【C++】智能指针

2023-10-17 08:43:25 浏览数 (2)

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

在我们异常一节就已经讲过,当使用异常的时候,几个函数层层嵌套,其中如果抛异常就可能导致没有释放堆区开辟的空间。这样就很容易导致内存泄漏。关于内存泄漏,我也曾在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表达式。

0 人点赞