C++11 --- 智能指针详解

一、智能指针的使用场景分析

在我们需要动态申请内存时,难免最后会有忘记释放内存的时候,这就导致了内存泄漏。在使用到异常时,某个函数抛出异常后,很可能前面申请的空间也未释放,因此也导致内存泄漏。
例如:

场景1:普通情况下申请空间后忘记释放

void test() {
	int* p = new int(10);
	double* pp = new double(1.1);
}
int main() {
	test();

	return  0;
}

场景2:抛出异常后,申请的空间无法释放

下⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后⾯的delete没有得到执⾏,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很⿇烦。智能指针放到这样的场景⾥⾯就让问题简单多了

double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
}
void test()
{
	// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
	// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
	// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
	// 是智能指针。
	int* array1 = new int[10];
	int* array2 = new int[10]; // 抛异常呢
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array1 << endl;
		cout << "delete []" << array2 << endl;	
		delete[] array1;
		delete[] array2;
		throw; // 异常重新抛出,捕获到什么抛出什么
	}
	// ...
	cout << "delete []" << array1 << endl;
	delete[] array1;
	cout << "delete []" << array2 << endl;
	delete[] array2;
}

二、RAII和智能指针的设计思路

RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想本质是
⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏
。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

智能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀
样,重载 operator*/operator->/operator[] 等运算符,⽅便访问资源。

设计一个简单的智能指针解决上面抛异常导致资源无法释放的问题:

template<class T>
struct SmartPtr {
	SmartPtr(T* ptr) :_ptr(ptr) {

	}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
	T& operator[](size_t pos) {
		return *(_ptr + pos);
	}

	~SmartPtr() {
		cout << "delete[] ..." << endl;
		delete[] _ptr;
	}
	T* _ptr = nullptr;
};
double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
}
void Func()
{
	SmartPtr<int> sp1 = new int[10];
	SmartPtr<int> sp2 = new int[10];

	
	int len, time;
	cin >> len >> time;
	cout << Divide(len, time) << endl;

	//不需要再手动释放资源
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

运行结果:
在这里插入图片描述

三、智能指针的本质及衍生的问题

在这里插入图片描述

四、C++标准库的智能指针的使用

C++标准库中的智能指针都在< memory >这个头⽂件下⾯,我们包含< memory >就可以是使⽤了,智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,它们的区别在原理上⽽⾔主要是解决智能指针拷⻉时的思路不同

  1. auto_ptr是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象,这是⼀个⾮常糟糕的设计,因为它会让被拷⻉对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使⽤auto_ptr。
    在这里插入图片描述
    在这里插入图片描述

  2. unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不⽀持拷
    ⻉,只⽀持移动(即,将被移动的指针进行资源交换,通常被移动的指针会被置空)。如果不需要拷⻉的场景就⾮常建议使⽤他。
    在这里插入图片描述
    在这里插入图片描述

  3. shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉,也⽀持移动。如果需要拷⻉的场景就需要使⽤他了。底层是⽤引⽤计数的⽅式实现的。
    在这里插入图片描述

shared_ptr也支持移动:
在这里插入图片描述

  1. shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值
    直接构造。

shared_ptr的多种构造方式如下:

struct Person {
	int _age;
	string _name;
};
void test2() {
	shared_ptr<Person> sp1(new Person({ 19,"lwx" }));
	shared_ptr<Person> sp2 = make_shared<Person>(29,"hlp");
	auto sp3 = make_shared<Person>(39, "lpo");
	shared_ptr<Person> sp4;
	//不可以,报错!!!
	shared_ptr<Person> sp5 = new Person({ 29,"cda" }); 
}
  1. shared_ptr 和 unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个
    空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。
    在这里插入图片描述

五、智能指针的原理(模拟实现)

1. auto_ptr的模拟实现

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. unique_ptr的模拟实现

template<class T>
class unique_ptr
{
public:
	explicit 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>& sp) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

	unique_ptr(unique_ptr<T>&& sp)  //移动构造
		:_ptr(sp._ptr)
	{
		sp._ptr = nullptr;
	}
	unique_ptr<T>& operator=(unique_ptr<T>&& sp) //移动赋值
	{
		delete _ptr;
		_ptr = sp._ptr;
		sp._ptr = nullptr;
	}
private:
	T* _ptr;
};

3. shared_ptr的模拟实现(简单版)

template<class T>
class shared_ptr {
public:
	//默认构造与有参构造
    shared_ptr(T* ptr = nullptr)
		:_ptr(ptr), _pcount(new int(1)) {

	}
	//拷贝构造
	shared_ptr(const shared_ptr<T>& sp) {
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		(*_pcount)++;
	}
	//拷贝赋值
	void operator= (const shared_ptr<T>& sp) {

		if (_ptr != sp._ptr) {  //忽略自己给自己赋值
			// 和本身就已经共同管理一块空间的对象之间的赋值

			if ((*_pcount)-- == 1) {  //本对象原本是一块资源的最后管理者
				                      //(即使不是,也做到了减去了一个管理者)
				delete _ptr;          //释放原来的资源
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			(*_pcount)++;
		}
	}
	~shared_ptr() {

		if ((*_pcount)-- == 1) {  //如果(*_pcount)==1,说明本对象
			//是某块资源的最后管理者,判断完后要--
			cout << "delete _ptr..." << endl;
			delete _ptr;
			delete _pcount;
		}
	}
	//使用:
	T* get() const	
	{
		return _ptr;
	}
	int use_count() const
	{
		return *_pcount;
	}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
	T& operator[](size_t pos) {
		return _ptr[pos];
	}
private:
	T* _ptr;
	int* _pcount;
};

注意:计数器不能用一个静态成员来充当!!!
在这里插入图片描述

六、定制删除器

1. 默认delete释放资源的不足

对于上面简单版的shared_ptr,一般场景下的使用是没问题的,但是如果资源是new[] 出来的就会发生错误,因为new[] 要与delete[] 搭配。
标准库的解决方案是特化出delete[]的版本。
使用:

std::shared_ptr<int[]> sp(new int[10]);

但是,假如这个指针要管理的资源是一个文件呢?文件最终不是被delete的,而是用fclose()关闭文件。如:

std::shared_ptr<FILE> sp(fopen("test.cpp","w"));

这个场景下,普通的shared_ptr是无法解决问题的,即文件打开了,确无法自动关闭。

因此,智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤对象这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调⽤删除器去释放资源。

标准库里shared_ptr支持定制删除器:
在这里插入图片描述
注: 第一个模版参数U是资源的类型,第二个模版参数D是删除器的类型。
p是资源的指针,del是一个对象。

2. 定制删除器的类型

删除器的类型可以有很多种,可以是仿函数,可以是函数指针,也可以是lambda表达式

  1. 例如:仿函数版本的删除器:
struct Fclose {
	void operator()(FILE* ptr) {
		fclose(ptr);
		cout << "fclose..." << endl;
	}
};
void test3() {

	std::shared_ptr<FILE> sp(fopen("test.cpp", "w"),Fclose());

}

该例子的调用逻辑解析:
在这里插入图片描述

  1. 再例如:以lambda表达式为类型的定制删除器:
    在这里插入图片描述

3. 比较完整的shared_ptr的实现

template<class T>
class shared_ptr {
public:
	//默认构造与有参构造
    explicit shared_ptr(T* ptr = nullptr)
		:_ptr(ptr), _pcount(new int(1)) {

	}

	//注:
	//这里如果构造时参数只有一个,就走上面的构造,如果
	//有两个就走下面这个:
	template<class D>
	shared_ptr(T* ptr, const D& del)
		: _ptr(ptr),
		 _del(del),
		 _pcount(new int(1)) {

	}


	//拷贝构造
	shared_ptr(const shared_ptr<T>& sp) {
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		(*_pcount)++;
	}
	//拷贝赋值
	void operator= (const shared_ptr<T>& sp) {

		if (_ptr != sp._ptr) {  //忽略自己给自己赋值
			// 和本身就已经共同管理一块空间的对象之间的赋值

			if ((*_pcount)-- == 1) {  //本对象原本是一块资源的最后管理者
				                      //(即使不是,也做到了减去了一个管理者)
				delete _ptr;          //释放原来的资源
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			(*_pcount)++;
		}
	}

	~shared_ptr() {

		if ((*_pcount)-- == 1) {  //如果(*_pcount)==1,说明本对象
			//是某块资源的最后管理者,判断完后要--
			cout << "delete _ptr..." << endl;

			//delete _ptr;
			
			//用删除器:
			_del(_ptr);
			delete _pcount;
		}
	}

	
	//使用:
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
	T& operator[](size_t pos) {
		return _ptr[pos];
	}

private:
	T* _ptr;
	int* _pcount;


	//包装器封装_del (因为这里无法拿到D来定义_del):
	function<void(T*)> _del = [](T* ptr) {delete ptr; };

	//这里默认给_del赋值为一个lambda表达式对象是为了迎合
	//当只有一个参数构造时(简单来说就是普通的new空间时,
	//                因为走的是上面的普通构造,没有构造出_del),
	//也一样可以析构时用删除器_del。
};

注:不支持下面这种构造方法的原因:
在这里插入图片描述

七、shared_ptr循环引用问题和weak_ptr

shared_ptr⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉。但是在循环引⽤的场景下会
导致资源没得到释放内存泄漏,所以我们要认识循环引⽤的场景和资源没释放的原因,并且学会使
⽤weak_ptr解决这种问题。

1. 问题的产生

例如:有下面这样一个场景:

在这里插入图片描述
连接成双向链表:
在这里插入图片描述
修改成:
在这里插入图片描述
结果发生内存泄漏:
在这里插入图片描述

2. 分析原因

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. 解决问题

把ListNode结构体中的_next和_prev的类型改成weak_ptr,weak_ptr指向shared_ptr所管理的资源时不会增加它的引⽤计数,因此_next和_prev不参与资源释放管理逻辑,就成功打破了循环引⽤,解决了这⾥的问题。
在这里插入图片描述

4. weak_ptr的原理

weak_ptr不⽀持RAII,也不⽀持访问资源,所以weak_ptr构造时不⽀持绑定到资源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引⽤问题。
在这里插入图片描述

4.1 weak_ptr的简单实现

注:

  1. 这里是对标准库的weak_ptr做了很大的阉割了,库里的实现更为复杂。
  2. weak_ptr不需要析构函数,因为它不需要管理和释放资源。
  3. 标准库里的weak_ptr也是有计数器的,因为即使weak_ptr它不管理资源,但是它也应该知道这块资源有几个管理者。
template<class T>
class weak_ptr {
public:
	weak_ptr() 
	{}
	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get())
	{}
	void operator=(const shared_ptr<T>& sp){
		_ptr(sp.get());
	}
	//不需要析构函数,因为它不需要管理和释放资源
private:
	T* _ptr=nullptr;
};

4.2 weak_ptr的一些成员函数

  1. weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的
    shared_ptr已经释放了资源,那么他去访问资源就是很危险的。
  2. weak_ptr有expired成员函数去检查指向的资源是否过期,use_count也可获取shared_ptr的引⽤计数。
  3. weak_ptr想访问资源时,可以调⽤lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
int main() {
		std::shared_ptr<string> sp1(new string("111111"));
		std::shared_ptr<string> sp2(sp1);
		std::weak_ptr<string> wp = sp1;

		//1.检查wp所指向的资源是否过期:过期返回1,未过期返回0:
		cout << wp.expired() << endl;  

		//2.查看wp指向的资源有几个shared_ptr对象在管理:
		cout << wp.use_count() << endl;


		// sp1和sp2都指向了其他资源,则weak_ptr就过期了
		sp1 = make_shared<string>("222222");
		cout << wp.expired() << endl;
		cout << wp.use_count() << endl;
		sp2 = make_shared<string>("333333");
		cout << wp.expired() << endl;
		cout << wp.use_count() << endl;


		wp = sp1;
		//std::shared_ptr<string> sp3 = wp.lock();
		//将sp1的资源锁住,并交给另一个shared_ptr对象(sp3):
		auto sp3 = wp.lock();
		cout << wp.expired() << endl;
		cout << wp.use_count() << endl;
		*sp3 += "###";  
		cout << *sp1 << endl;
		return 0;
}
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值