C++之智能指针

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

如果在 div() 输入的 b == 0,那么就会抛出一个异常,被 main() 捕获,但是在 Func() 中 new 申请的资源就会因没释放而发生泄露问题,这是一种异常安全问题。

#include <iostream>
using namespace std;

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void Func()
{
	int* p = new int;

	cout << div() << endl;  // 异常安全问题

	cout << "delete:" << p << endl;
	delete p;
}

int main()
{
	try
	{
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

那么为了处理这里的异常安全问题,可以在 Func() 中捕获异常然后释放资源,再重新抛出,但这种方式并没有从根源上解决问题。因为 new 可能存在多个并且也有可能抛异常,那么在这种情况下,就很难判断是谁抛的异常。所以,当多个可能会抛异常的地方交织在一起的时候,这种捕获再重新抛出的方式会让处理者处理得焦头烂额。

因此,C++ 引入了智能指针。

二、智能指针的使用及原理

1. RAII

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

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构时释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:一是不需要显式地释放资源。二是采用这种方式,对象所需的资源在其生命周期内始终保持有效。

2.智能指针的原理

总结一下智能指针的原理:
 ① RAII 特性。
 ② 重载 operator* 和 opertaor-> ,具有像指针一样的行为。

下面是我们简单设计的智能指针:

//RAII
//用起来像指针一样
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;  //测试用
			delete _ptr;
		}
	}

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

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

private:
	T* _ptr;
};


int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	SmartPtr<int> sp3(new int);

	*sp1 = 10;
	cout << *sp1 << endl;
	(*sp1)++;
	(*sp1)++;
	cout << *sp1 << endl;

	cout << div() << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

但是我们设计的智能指针有一个问题,就是拷贝问题。
我们没有实现它的拷贝构造函数,所以是浅拷贝。两个对象最后销毁时各调用了一次析构函数,结果就是对同一块空间释放了两次,导致程序崩溃。

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

	return 0;
}

那么该如何解决拷贝问题呢?

3. auto_ptr

为了处理这个问题,C++98 中智能指针 auto_ptr 的解决方案是管理权转移。
既然两个对象指向同一块空间最后会析构两次,如果永远只有一个对象指向一块空间,那么就不会出现上述问题了。
所以当一个对象拷贝构造另一个对象时,先进行值拷贝,然后把原对象置空,这就实现了管理权的转移。

// C++98 管理权转移 auto_ptr
template<class T>
class auto_ptr    //我们简化模拟的auto_ptr
{
public:
	auto_ptr(T* ptr = nullptr)
		:_ptr(ptr)
	{}

	//拷贝构造函数
	auto_ptr(auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;  //管理权转移
	}

	//赋值重载函数
	auto_ptr<T>& operator=(auto_ptr<T>& ap)
	{
		if (this != &ap)
		{
			if (_ptr)  // 释放当前对象的资源
			{
				delete _ptr;
			}

			_ptr = ap._ptr;  // 转移资源到当前对象中
			ap._ptr = nullptr;
		}

		return *this;
	}

	~auto_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;  //测试用
			delete _ptr;
		}
	}

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

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

private:
	T* _ptr;
};

int main()
{
	auto_ptr<int> ap1(new int);
	auto_ptr<int> ap2(ap1);  //管理权转移
	
	return 0;
}

虽然 auto_ptr 这样设计解决了拷贝问题,但同时也出现了一个很大的问题,就是原对象悬空了。如果不小心访问了它,就会出现访问空指针问题,导致程序崩溃。

int main()
{
	auto_ptr<int> ap1(new int);
	auto_ptr<int> ap2(ap1);  //管理权转移
	
	//ap1悬空
	*ap2 = 10;
	cout << *ap2 << endl;
	cout << *ap1 << endl;  //不小心访问了ap1
	
	return 0;
}

结论:auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_ptr 。

4. unique_ptr

unique_ptr 是防拷贝和防赋值的智能指针。

// C++11库才更新智能指针实现
// C++11出来之前,boost -> scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11 -> unique_ptr/shared_ptr/weak_ptr

// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
template<class T>
class unique_ptr    //我们简化模拟的unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~unique_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;  //测试用
			delete _ptr;
		}
	}

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

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

	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

private:
	T* _ptr;
};

int main()
{
	unique_ptr<int> up1(new int);
	/*unique_ptr<int> up2(up1);*/  // 会编译报错,因为防拷贝

	return 0;
}

5. shared_ptr

在某些场景下,需要用到支持拷贝的智能指针。

shared_ptr 的原理:引用计数支持拷贝

在这里插入图片描述

引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源。

  1. shared_ptr 在其内部,给每份资源都维护了一份引用计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,该资源的引用计数减一。如果引用计数是 0 ,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是 0 ,就说明除了自己还有其他对象在使用该份资源,不能释放该资源。
  3. 引用计数有线程安全问题,是智能指针本身需要处理的,所以需要使用互斥锁来进行维护。
template<class T>
class shared_ptr    //我们简化模拟的shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pRefCount(new int(1))
		, _pmtx(new mutex)
	{}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pRefCount(sp._pRefCount)
		,_pmtx(sp._pmtx)
	{
		AddRef();  //引用计数+1
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)  //比较内部的指针才能真正避免自己给自己赋值
		{
			Release();  //释放资源

			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			_pmtx = sp._pmtx;

			AddRef();  //引用计数+1
		}

		return *this;
	}

	int use_count()
	{
		return *_pRefCount;
	}

	~shared_ptr()
	{
		Release();
	}

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

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

	T* get() const
	{
		return _ptr;
	}

private:
	void Release()  //释放资源
	{
		_pmtx->lock();  //保证引用计数的安全性
		
		bool flag = false;  //flag是局部变量,用于判断是否释放锁
		if (--(*_pRefCount) == 0 && _ptr)  //引用计数-1,并判断是否是0
		{
			cout << "delete:" << _ptr << endl;  //测试用
			delete _ptr;
			delete _pRefCount;

			flag = true;
		}

		_pmtx->unlock();

		if (flag == true)
		{
			delete _pmtx;  //mutex是new来的,最后需要delete,只能在解锁后delete
		}
	}

	void AddRef()  //引用计数+1
	{
		_pmtx->lock();  //保证引用计数的安全性

		++(*_pRefCount);

		_pmtx->unlock();
	}

private:
	T* _ptr;          //指向资源
	int* _pRefCount;  //指向引用计数
	mutex* _pmtx;     //指向互斥锁,用于维护引用计数的安全性
};

测试代码1:

int main()
{
	shared_ptr<int> sp1(new int);
	shared_ptr<int> sp2(sp1);
	shared_ptr<int> sp3(sp1);

	shared_ptr<int> sp4(new int);
	shared_ptr<int> sp5(sp4);

	//sp1和sp2指向同一份资源
	sp1 = sp1;  //自己给自己赋值
	sp1 = sp2;  //自己给自己赋值

	sp1 = sp4;
	sp2 = sp4;
	sp3 = sp4;
	
	*sp1 = 2;
	*sp2 = 3;

	return 0;
}

测试代码2:

struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};


// shared_ptr智能指针内部引用计数的加减是加锁保护的,所以是线程安全的
// 但是指向的资源不是线程安全的
// 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了
void SharePtrFunc(shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
	cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		//内部引用计数的加减是线程安全的
		shared_ptr<Date> copy(sp);  

		//指向的资源不是线程安全的,需要自行加锁保护
		//只有这部分需要锁,后面的部分不需要锁
		//因此,我们可以把这部分括起来,特地弄成一个局部域
		//这样的话,对象出了作用域就会销毁,自动解锁
		{
			unique_lock<mutex> lk(mtx);
			copy->_year++;
			copy->_month++;
			copy->_day++;
		}

		// ...
	}
}

int main()
{
	shared_ptr<Date> sp(new Date);
	cout << sp.get() << endl;
	const size_t n = 100000;
	mutex mtx;
	thread t1(SharePtrFunc, std::ref(sp), n, std::ref(mtx));
	thread t2(SharePtrFunc, std::ref(sp), n, std::ref(mtx));

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

	cout << sp->_year << endl;
	cout << sp->_month << endl;
	cout << sp->_day << endl;

	cout << sp.use_count() << endl;
	
	return 0;
}

但是 shared_ptr 在某种场景下会出现循环引用的问题。
比如下面的代码:

struct ListNode
{
	int _val;
	std::shared_ptr<ListNode> _prev;
	std::shared_ptr<ListNode> _next;

	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);

	cout << n1.use_count() << endl;  // 1
	cout << n2.use_count() << endl;  // 1
	
	// 循环引用
	n1->_next = n2;
	n2->_prev = n1;

	cout << n1.use_count() << endl;  // 2
	cout << n2.use_count() << endl;  // 2
	
	return 0;
}

我们的预期结果是,最终两个节点都会被释放,即会调用两次 ListNode 的析构函数。
但实际的运行结果是,两个节点都没有被释放,即 ListNode 的析构函数一次都没有调用。

循环引用分析:
在这里插入图片描述

  1. n1 和 n2 两个智能指针对象各指向一个节点,引用计数都是 1 。
  2. 经过赋值后,n1 的 _next 指向 n2 所指向的节点,n2 的 _prev 指向 n1 所指向的节点,引用计数都变成 2 。
  3. 最后 n2 和 n1 先后析构,引用计数都减到 1 。此时 _next 指向下一个节点,_prev 指向上一个节点,于是就形成了这样的局面:两个节点最终都不会释放,造成内存泄漏的问题。
  4. 换言之,节点 2 的释放取决于 _next 的析构,_next 的析构取决于节点 1 的释放,节点 1 的释放取决于 _prev 的析构,_prev 的析构取决于节点 2 的释放,所以就形成了一个永远解不开的环,这就是循环引用。

shared_ptr 很好,但就是有循环引用的问题,设计者也没有很好的办法,于是之后又设计了专门应对这种情况的 weak_ptr 。

6. weak_ptr

weak_ptr 不是常规意义的智能指针,它没有一个接收原生指针的构造函数,也不符合 RAII 。

weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它可以从一个 shared_ptr 或另一个 weak_ptr 对象来构造,它的构造和析构不会引起 shared_ptr 引用记数的增加或减少(不参与资源的释放管理),所以它可以解决 shared_ptr 循环引用的问题

构造函数:在这里插入图片描述

它的use_count()返回的是 shared_ptr 的引用计数。
在这里插入图片描述

template<class T>
class weak_ptr    //我们简化模拟的weak_ptr
{
public:
	weak_ptr()
		:_ptr(nullptr)
	{}

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

	weak_ptr(const weak_ptr<T>& wp)
		:_ptr(wp._ptr)
	{}

	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();

		return *this;
	}

	weak_ptr<T>& operator=(const weak_ptr<T>& wp)
	{
		_ptr = wp._ptr;

		return *this;
	}

private:
	T* _ptr;
};

weak_ptr 解决了循环引用的问题:

struct ListNode
{
	int _val;
	//std::shared_ptr<ListNode> _prev;
	//std::shared_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	std::weak_ptr<ListNode> _next;

	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);

	cout << n1.use_count() << endl;  // 1
	cout << n2.use_count() << endl;  // 1

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

	cout << n1.use_count() << endl;  // 1
	cout << n2.use_count() << endl;  // 1
	
	return 0;
}

运行结果是,最终两个节点都会被释放,即调用了两次 ListNode 的析构函数。

7.删除器

new 和 delete 需要匹配使用:new 和 delete 、new[ ] 和 delete[ ] 。否则可能会报错。

如果我们使用了 new[ ] ,但最后使用 delete 而非 delete[ ] ,并且要释放资源对象的内部实现了析构函数,那么一定会运行出错。

智能指针里面有删除器,删除器是一个可调用对象。
智能指针默认使用默认删除器,而默认删除器使用的是 delete ,当我们使用 new[ ] 时,就会运行出错。

class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
	
private:
	int _a1 = 0;
	int _a2 = 0;
};

int main()
{
	std::unique_ptr<A> up1(new A);      // 没有问题
	std::unique_ptr<A> up2(new A[10]);  // 有问题

	return 0;
}

但是我们申请的资源有可能不是 new 出来的,比如:new[ ]、malloc、fopen 。
那么该如何解决这种情况呢?这就需要我们定制删除器来解决了。

//定制删除器
template<class T>
struct DeleteArray
{
	void operator()(const T* ptr)
	{
		cout << "delete[]:" << ptr << endl;  // 测试用
		delete[] ptr;
	}
};

//定制删除器
struct DeleteFile
{
	void operator()(FILE* ptr)
	{
		cout << "fclose:" << ptr << endl;  // 测试用
		fclose(ptr);
	}
};

int main()
{
	//删除器在类模板参数给 -- 类型
	std::unique_ptr<A> up1(new A);
	std::unique_ptr<A, DeleteArray<A>> up2(new A[10]);
	std::unique_ptr<FILE, DeleteFile> up3(fopen("test.txt", "w"));

	//删除器在构造函数的参数给 -- 对象
	std::shared_ptr<A> sp1(new A);
	std::shared_ptr<A> sp2(new A[10], DeleteArray<A>());
	std::shared_ptr<FILE> sp3(fopen("test.txt", "w"), DeleteFile());
	
	std::shared_ptr<A> sp4(new A[10], [](A* p) {delete[] p; });
	std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p) {fclose(p); });

	return 0;
}

我们用 unique_ptr 来简单模拟一下:

//定制删除器
template<class T>
struct DeleteArray
{
	void operator()(const T* ptr)
	{
		cout << "delete[]:" << ptr << endl;  // 测试用
		delete[] ptr;
	}
};

//定制删除器
struct DeleteFile
{
	void operator()(FILE* ptr)
	{
		cout << "fclose:" << ptr << endl;  // 测试用
		fclose(ptr);
	}
};

namespace MyLib
{
	template<class T>
	class default_delete
	{
	public:
		void operator()(const T* ptr)
		{
			cout << "delete:" << ptr << endl;  // 测试用
			delete ptr;
		}
	};

	//释放方式由D删除器决定
	template<class T, class D = default_delete<T>>
	class unique_ptr    //我们简化模拟的unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			if (_ptr)
			{
				//cout << "delete:" << _ptr << endl;  //测试用
				//delete _ptr;

				D del;
				del(_ptr);
			}
		}

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

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

		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

	private:
		T* _ptr;
	};
}

int main()
{
	MyLib::unique_ptr<A> up1(new A);
	MyLib::unique_ptr<A, DeleteArray<A>> up2(new A[10]);
	MyLib::unique_ptr<FILE, DeleteFile> up3(fopen("test.txt", "w"));
	
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值