【C++】智能指针


1. RAII

💭智能指针是用以资源管理的一种工具。所谓资源就是,一旦用了它,将来就必须还给系统。如果不这样的话,会发生糟糕的事,如内存泄漏。C++程序中最常见的资源就是动态分配的内存(new/malloc申请堆上的内存,delete/free释放,如果不释放会导致内存泄漏)。谨慎地编写程序能让我们避免大部分不将资源还给系统的情况,但还是会有一些情况是难以避免的,举两个例子:

⭕情况1:func1()中,因为抛异常而未能执行语句delete[] pArray,资源未释放

void func1()
{
	int* pArray = new int[10];

	// to do something...
	throw("error");

	delete[] pArray;
}

int main()
{
	try
	{
		func1();
	}
	catch (const char* errmsg)
	{
	}

	return 0;
}

⭕情况2:函数内多重回传路径

void func1(int x)
{
	int* pArray = new int[10];

	if (x > 0)
	{
		return;
	}
	else
	{
		std::cout << x << std::endl;
	}

	delete[] pArray;
}

int main()
{
	int x = 0;
	func1(x);

	return 0;
}

事实上,内存只是你管理的众多资源之一。其它资源如:打开的文件描述符(File Describtors)、互斥量(Mutex)、网络sockets等在使用后都需要归还给系统。而仅仅让程序员谨慎代码是很难完全规避内存泄漏的问题的。

  • RAII思想

    RAII(Resource Acquisiton Is Initialization),是一种利用对象的生命周期管理资源的方法,即“资源取得时机便是初始化时机”。每一笔资源在被获取到时,立刻被放入管理对象中。而管理对象运用析构函数确保资源被释放。也就是说,无论执行流以任何形式离开当前区块,一旦管理对象被销毁,生命周期结束,既调用管理对象的析构函数,而析构函数中包含了对象管理的资源的释放方法,这样就能保证资源被释放了。

  • RAII的优势

    1. 不需要显式地释放资源
    2. 对象管理的资源在其生命周期内有效

而智能指针正是运用了RAII思想,对资源进行管理的一种对象。


2. 智能指针

智能指针,顾名思义,除了能够以RAII思想管理资源外,还要能够像原生指针一样使用,既通过*->访问管理资源,在C++类中可通过运算符重载实现。而智能指针真正的难点在于拷贝问题,不同的智能指针实现对拷贝问题有不同的解决方法。

🔎下面介绍C++标准库中三种智能指针:

头文件:#include <memory>

auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针,下面演示auto_ptr的模拟实现。

拷贝问题解决方法:资源管理权的交换

在这里插入图片描述

auto_ptr模拟实现

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

        // 拷贝构造,直接取ap的指针,并将ap指针设为空
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

        // 赋值重载
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			//交换资源管理权,即交换指针
			std::swap(ap._ptr, _ptr);
			return *this;
		}

		~auto_ptr()
		{
            // 析构时释放资源
			if (_ptr)
				delete _ptr;
		}

        // 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

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

	private:
		T* _ptr; // 指针管理资源
	};
}

auto_ptr的缺陷:

  1. 多个auto_ptr无法指向同一资源的问题。(若多个auto_ptr指向同一资源,资源会被删除多次,这是错误的)
  2. 频繁交换资源管理权,会导致auto_ptr的指向不明确。

综上所述,auto_ptr是一个失败的设计。


unique_ptr

C++11库中给出的更靠谱的unique_ptr

拷贝问题解决方法:简单粗暴的禁止拷贝

unique_ptr模拟实现

namespace ckf
{
	template <class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
        
        // 删除拷贝构造和赋值重载函数
		unique_ptr(const unique_ptr& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr& up) = delete;

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

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

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

	private:
		T* _ptr;
	};
}

unique_ptr的缺陷:

​ 比起auto_ptr,指针的指向非常明确,但依然无法解决多个指针资源共享的问题。

shared_ptr

C++11中的shared_ptr完美解决了以上两个智能指针的问题。

拷贝问题解决方法:引用计数

在这里插入图片描述

  • shared_ptr模拟实现
namespace ckf
{
	// 1. RAII
	// 2. like pointer
	// 3. copy

	template <class T>
	class shared_ptr
	{
		typedef shared_ptr<T> Self;
	public:

		T& operator*()
		{
			assert(_pdata);
			return *_pdata;
		}

		T* operator->()
		{
			assert(_pdata);
			return _pdata;
		}


		// constructor
		shared_ptr(T* pdata = nullptr)
			:_pdata(pdata)
		{
			if (_pdata)
			{
				_pcount = new int(1);
				_pmtx = new std::mutex;
			}
			else
			{
                // 若指向资源为空,则计数器和互斥量也为空
				_pcount = nullptr;
				_pmtx = nullptr;
			}
		}

		// destructor
		~shared_ptr()
		{
            // release先对计数器减1,再检查计数器是否为0,为0则释放资源
			release();
		}

		// copy
		shared_ptr(const Self& sp)
			:_pdata(sp._pdata)
			, _pcount(sp._pcount)
			, _pmtx(sp._pmtx)
		{
			if(_pdata)
				add_count();
		}

		// operator=
		Self& operator=(const Self& sp)
		{
			// 相同就不用赋值了
			if (_pdata != sp._pdata)
			{
				release();

				_pdata = sp._pdata;
				_pcount = sp._pcount;
				_pmtx = sp._pmtx;
				
				if (_pdata)
					add_count();
			}

			return *this;
		}
		
        // 获取计数器内容(指向为空返回0)
		int use_count() const
		{
			if (_pdata)
				return *_pcount;
			else
				return 0;
		}

		T* get() const
		{
			return _pdata;
		}

	private:

		void release()
		{
			if (_pdata) // 若为空,什么也不做
			{
                // 访问计数器,需要加锁保护
				_pmtx->lock();

				bool deleteFlag = false;

				// 计数器减到0,即释放资源
				if (--(*_pcount) == 0)
				{
					delete _pdata;
					delete _pcount;
					deleteFlag = true;
				}

				_pmtx->unlock();
				if (deleteFlag) // 资源释放,锁才释放
					delete _pmtx;
			}
		}

		void add_count()
		{
            // 访问计数器,需要加锁保护
			_pmtx->lock();

			++(*_pcount);

			_pmtx->unlock();
		}

		T* _pdata;
		int* _pcount;
		std::mutex* _pmtx;
	};

	// 多线程可能会使用同一个shared_ptr(包括它的拷贝)
	// 因此
	// 对于计数器的访问,应该由shared_ptr保证线程安全
	// 对于sp指向数据,应该由用户自行保证数据安全
}
  • shared_ptr赋值的过程:

在这里插入图片描述

  • shared_ptr线程安全的测试demo
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std;

class Date
{
public:
	Date(int year = 2023, int month = 9, int day = 7)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	~Date()
	{
		std::cout << "~Date()" << std::endl;
	}

	void getDate()
	{
		printf("%d年%d月%d日\n", _year, _month, _day);
	}

	int _year;
	int _month;
	int _day;
};

void SmartPtrTest(ckf::shared_ptr<Date>& sp, const int& n, std::mutex& mtx)
{
	// 测试ckf::shared_ptr的线程安全
	for (int i = 0; i < n; i++)
	{
		// 这里两个线程不断拷贝sp和释放sp,既不断对sp计数器的++和--,最终保证sp计数器为1
		ckf::shared_ptr<Date> copy(sp);

		// 管理资源的安全由程序员保证
		// 若对资源的访问是线程安全的
		// 那么两个线程执行后三个值应该都是20000
		mtx.lock();

		copy->_year++;
		copy->_month++;
		copy->_day++;

		mtx.unlock();
	}

	//这里出现2的原因:
	//可能是另外一个线程正处于for循环中创建copy和销毁copy的间隙
	//此时计数器为2
	//线程调度到当前线程,use_count()打印的数就是2
	//但是不影响最终计数器依旧为1
	//std::cout << sp.get() << " " << sp.use_count() << std::endl;
}

void test_thread_safe()
{
	ckf::shared_ptr<Date> sp(new Date(0, 0, 0));
	const int n = 100000;
	std::mutex mtx;

	// 观察多线程执行前后,sp计数器是否依然为1
	cout << "before: " << sp.use_count() << endl;
	sp->getDate();
	cout << endl;

	std::thread t1(SmartPtrTest, std::ref(sp), n, std::ref(mtx));
	std::thread t2(SmartPtrTest, std::ref(sp), n, std::ref(mtx));
	std::this_thread::sleep_for(std::chrono::seconds(1));

	cout << "after: " << sp.use_count() << endl;
	sp->getDate();
	cout << endl;

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

int main()
{
	test_thread_safe();
	return 0;
}

⭕运行结果

在这里插入图片描述

  • 删除器deleter

有时候,单单的delete ptr无法满足释放资源的需求。就如:管理的资源是以new T[]形式开辟的,则需delete[]释放;又如:管理的资源是C语言打开文件的FILE结构体,那么需要fclose()函数释放。标准库中的shared_ptr配套了删除器,用于指定某种释放资源的方式。

在这里插入图片描述

使用方法如下:

void test_deleter()
{
    // 事实上deleter就是一个可调用对象,这里用了lambda表达式
	std::shared_ptr<Date> sp1(new Date[10], [](Date* ptr) {delete[] ptr; });
	std::shared_ptr<FILE> sp2(fopen("log.txt", "w"), [](FILE* fp) {fclose(fp); });
}

shared_ptr with deleter

namespace ckf
{
	// 1. RAII
	// 2. like pointer
	// 3. copy

	template <class T>
	class shared_ptr
	{
		typedef shared_ptr<T> Self;
	public:

		T& operator*()
		{
			assert(_pdata);
			return *_pdata;
		}

		T* operator->()
		{
			assert(_pdata);
			return _pdata;
		}


		// constructor
		shared_ptr(T* pdata = nullptr)
			:_pdata(pdata)
		{
			if (_pdata)
			{
				_pcount = new int(1);
				_pmtx = new std::mutex;
			}
			else
			{
				_pcount = nullptr;
				_pmtx = nullptr;
			}
		}
		
        // 增加一个构造函数,可自动推导deleter类型
		template <class D>
		shared_ptr(T* pdata, D del)
			:_pdata(pdata)
			,_del(del)
		{
			if (_pdata)
			{
				_pcount = new int(1);
				_pmtx = new std::mutex;
			}
			else
			{
				_pcount = nullptr;
				_pmtx = nullptr;
			}
		}

		// destructor
		~shared_ptr()
		{
			release();
		}
		
        // deleter也要拷贝
		// copy
		shared_ptr(const Self& sp)
			:_pdata(sp._pdata)
			, _pcount(sp._pcount)
			, _pmtx(sp._pmtx)
			, _del(sp._del)
		{
			if(_pdata)
				add_count();
		}

		// operator=
		Self& operator=(const Self& sp)
		{
			// 相同就不用赋值了
			if (_pdata != sp._pdata)
			{
				release();

				_pdata = sp._pdata;
				_pcount = sp._pcount;
				_pmtx = sp._pmtx;
				_del = sp._del;
				
				if (_pdata)
					add_count();
			}

			return *this;
		}

		int use_count() const
		{
			if (_pdata)
				return *_pcount;
			else
				return 0;
		}

		T* get() const
		{
			return _pdata;
		}

	private:

		void release()
		{
			if (_pdata) // 若为空,什么也不做
			{
				_pmtx->lock();

				bool deleteFlag = false;

				// sp为空或者减计数到0,即释放空间
				if (--(*_pcount) == 0)
				{
					//delete _pdata;
					_del(_pdata);
					delete _pcount;
					deleteFlag = true;
				}

				_pmtx->unlock();
				if (deleteFlag)
					delete _pmtx;
			}
		}

		void add_count()
		{
			_pmtx->lock();

			++(*_pcount);

			_pmtx->unlock();
		}

		T* _pdata;
		int* _pcount;
		std::mutex* _pmtx;
		std::function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};
}

shared_ptr的缺陷:

shared_ptr是较为靠谱的一种智能指针,但是会有一个小问题——循环引用。看下面代码:

struct Node
{
	ckf::shared_ptr<Node> _next;
	ckf::shared_ptr<Node> _prev;

	Node(int val) :_val(val)
	{}

	~Node()
	{
		std::cout << _val << "~Node()" << std::endl;
	}
    
    int _val = 0;
};

void test_cycle_ref()
{
	ckf::shared_ptr<Node> n1(new Node(1));
	ckf::shared_ptr<Node> n2(new Node(2));

	n1->_next = n2;
	n2->_prev = n1;
}

int main()
{
    test_cycle_ref();
    return 0;
}

📑演示图:

在这里插入图片描述

流程分析:

  1. n1和n2两个shared_ptr分别指向节点1和节点2,以RAII方式管理资源。此时两个节点的计数器都为1。
  2. 两个节点中的_next和_prev也是shared_ptr,节点1的_next指向节点2,节点2的_prev指向节点1。此时两个节点的计数器都增加为2。
  3. 函数执行完毕,先销毁n2——节点2的计数器减为1(节点1中的_next还指向节点2),再销毁n1——节点1的计数器减为1(节点2的_prev还指向节点1)。
  4. 此时陷入了僵局。只有_next析构了,节点2才能释放,只有_prev析构了,节点1才能释放。根据类的特性,节点1释放,_next才会析构,节点2释放,_prev才会析构。二者的释放条件依赖于彼此,谁也不释放,这就是循环引用。(节点1释放要2.prev析构->2.prev析构要节点2释放->节点2释放要1.next析构->1.next析构要节点1释放,形成闭环

解决方法:

C++11中引进了weak_ptr智能指针。该类型指针通常不单独使用,只能和 shared_ptr搭配使用来解决循环引用问题。

  1. weak_ptr并不参与资源的管理,只是作为shared_ptr的辅助,与shared_ptr指向相同的资源,不会修改所管理资源配套的计数器。
  2. weak_ptr没有重载*->运算符,这意味着weak_ptr只能指向内存空间,主要用于保存或赋予指针给shared_ptr,无法访问修改内存空间。
  3. weak_ptr也有引用计数,但只用于查看与该weak_ptr指向相同的shared_ptr的数量。引用计数为0时,weak_ptr处于过期状态。
struct Node
{
	int _val = 0;
	std::weak_ptr<Node> _next;
	std::weak_ptr<Node> _prev;

	Node(int val) :_val(val)
	{}

	~Node()
	{
		std::cout << _val << "~Node()" << std::endl;
	}
};

void test_cycle_ref()
{
	std::shared_ptr<Node> n1(new Node(1));
	std::shared_ptr<Node> n2(new Node(2));

    // weak_ptr可以接收shared_ptr的赋值,也能以shared_ptr进行初始化构造
	n1->_next = n2; 
	n2->_prev = n1;
	
    // weak_ptr可以作为shared_ptr初始化构造的值,但是无法赋值给shared_ptr
	std::shared_ptr<Node> n3(n1->_next); // 此时n3与n2指向相同

	std::cout << n3.use_count() << std::endl;
	std::cout << (n1->_next).use_count() << std::endl;
}

int main()
{
    test_cycle_ref();
    return 0;
}

运行结果:

在这里插入图片描述


3. 智能指针历史

  1. C++ 98 中产生了第一个智能指针auto_ptr;
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr;
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版;
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

Ending…

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值