【C++学习笔记】C++智能指针!你绝对不能错过的干货!

【本节目标】

  • 1.为什么需要智能指针?

  • 2. 内存泄漏

  • 3.智能指针的使用及原理

  • 4.C++11和boost中智能指针的关系

  • 5.RAII扩展学习

1.为什么需要智能指针?

还记得我们上一篇文章的的问题吗?我们为了防止出现抛异常从而导致一些资源没有释放,我们申请空间就要向下面一样写?一言难尽呀,这个代码咋看咋难受啊!

// 下面程序及其难看
// 智能指针解决
void fxx()
{
	int* p1 = new int[10];
	int* p2, *p3;
	try
	{
		p2 = new int[20];
		try {
			p3 = new int[30];
		}
		catch (...)
		{
			delete[] p1;
			delete[] p2;
			throw;
		}
	}
	catch (...)
	{
		delete[] p1;
		throw;
	}
 
	//...
 
	delete[] p1;
	delete[] p2;
	delete[] p3;
}

下面我们先分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析MergeSort 函数中的问题。

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

1、如果p1这里new 抛异常会如何?p1空间还没申请,抛异常没有问题。

2、如果p2这里new 抛异常会如何?p1空间已经申请,抛异常造成p1内存泄漏。

3、如果div调用这里又会抛异常会如何? p1和p2空间都申请,抛异常造成p1和p2内存泄漏。

可想而知,我们为什么要学智能指针了,你还不往下学学智能指针,看看它到底有多智能?

2. 内存泄漏

2.1 什么是内存泄漏,内存泄漏的危害

        什么是内存泄漏:内存泄漏指因为疏忽错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

        内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;


	// 2.异常安全问题
	int* p3 = new int[10];

	Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.

	delete[] p3;
}

同时还有一种情况

int main()
{
    char* p1 = new char[1024 * 1024 * 1024];
    std::cout << p1 << std::endl;//char*默认打印字符串

    return 0;
}

此时会一直打印屯屯....,导致内存被占满,此时就需要p1强制转成void*。

2.2 内存泄漏分类(了解)

C/C++程序中一般我们关心两种方面的内存泄漏:

堆内存泄漏(Heap leak)

        堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统资源泄漏

        指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

2.3 如何检测内存泄漏(了解)

在linux下内存泄漏检测:linux下几款内存泄漏检测工具

在windows下使用第三方工具:VLD工具说明

其他工具:内存泄漏工具比较

2.4如何避免内存泄漏

1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps: 这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。

2. 采用RAII思想或者智能指针来管理资源。

3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

总结一下: 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄 漏检测工具。

3.智能指针的使用及原理

3.1 RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如申请的内存资源等等)的简单技术。

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

  • ⭐不需要显式地释放资源。
  • ⭐采用这种方式,对象所需的资源在其生命期内始终保持有效。

⭐资源的管理交给一个对象,而不需要我们自己去显示管理!

我们先来解决一下第一个超级难看的那个代码的问题

class SmartPtr
{
public:
	SmartPtr(int* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete[] _ptr" << endl;

		delete[] _ptr;
	}
	//行为像指针一样
	// operator*
	// operator->
	// operator[]

private:
	int* _ptr;
};

void fxx()
{
	SmartPtr sp1(new int[10]);
	SmartPtr sp2(new int[10]);
	SmartPtr sp3 = new int[10];//单参数隐式类型转换

	int x, y;
	cin >> x >> y;
	Division(x, y);
}

int main()
{
	try
	{
		fxx();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}

	return 0;
}

此时无论是sp1,还是sp2,或者是sp3,在或是Division,无论哪一个抛异常,我们的程序都不会出现内存泄漏的问题,而且代码的可观性是比我们之前写的要好看的多,毕竟我们写代码是要追求美观的。

然后我们再来解决一下上面内存泄漏的问题,为了能让不同的指针变量都能够借助对象来管理资源,所以我们可以对指针的类型使用我们的神器:模板

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		cout << "delete[] _ptr" << endl;
		delete[] _ptr;
	}
private:
	T* _ptr;
};
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	//int* p1 = (int*)malloc(sizeof(int));
	//int* p2 = new int;

	// 使用智能指针
	SmartPtr<int> p1 = (int*)malloc(sizeof(int));
	SmartPtr<int> p2 = new int;

	// 2.异常安全问题
	//int* p3 = new int[10];
	//div(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
	//delete[] p3;

	// 使用智能指针
	SmartPtr<int> p3 = new int[10];
	div();
}
int main()
{
	try
	{
		MemoryLeaks();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

3.2 智能指针的原理

我们用智能指针解决了资源的释放问题,但是此时我们要怎么使用这个空间呢?上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可 以通过->去访问所指空间中的内容,此时我们把资源都交给了智能指针对象了,那么自然也在智能指里面完成访问空间的操作,但是我们还是想和之前的使用方法一样,所以我们就要SmartPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
        : _ptr(ptr)
    {}
    ~SmartPtr()
    {
        if (_ptr)
            delete _ptr;
    }
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
private:
    T* _ptr;
};
struct Date
{
    int _year;
    int _month;
    int _day;
};
int main()
{
    SmartPtr<int> sp1(new int);
    *sp1 = 10;
    cout << *sp1 << endl;
    SmartPtr<pair<string, int>> sp2(new pair<string, int>);
    sp2->first = "apple";
    sp2.operator->()->second = 1;
    // 需要注意的是这里应该是sp2.operator->()->first = "apple";
    // 本来应该是sp2->->first这里语法上为了可读性,省略了一个->
    return 0;
}

总结一下智能指针的原理:

⭐1. RAII特性

⭐2. 重载operator*和opertaor->,具有像指针一样的行为。

3.3 std::auto_ptr

std::auto_ptr文档

我们首先来看一下智能指针的拷贝问题

此时很明显,因为我们没有写拷贝构造函数,此时使用的是编译器默认生成的拷贝构造函数,此时是浅拷贝,所以导致程序崩溃,那怎么办呢?我们写一个深拷贝的拷贝构造???

注意:我们的智能指针式模拟的原生指针的行为,它只是代管这个资源,这个资源是别人的只为了能够方便修改数据,他们拷贝的时候期望的是指向同一个资源,所以是浅拷贝,我们可以通俗一点讲,就是我们只对这个资源管理,如果是深拷贝,那不就无中生有了,深拷贝那就有两个资源了,这个资源本来就是别人的,我们不仅要管理别人的,现在还要管理这个深拷贝的,那不是给自己增加难度吗?我们拷贝就期望我们俩可以管理同一份资源。

我们之前的模拟迭代器的目的没有啥事儿是因为我们的迭代器只是修改数据,而我们的智能指针要求我们去释放资源,所以这里就会出现问题。于是呢~,咱们看看c++98库提供了智能指针拷贝是啥情况!

一看,哇,看来我的代码还是不过关,库里面的没事,我的还是崩溃了~~~那咱们借鉴一下库,调式一下,看看库里面是怎么做的。

哇趣,库里直接是把sp1的资源直接给sp2了,然后sp1自己就啥都没有了,这种叫做管理权转移,被拷贝对象把资源管理权转移给拷贝对象,这里是不是有点像c++右值的移动语义,它是将一个将亡值的资源给别人,但是我们这里的sp1不是将亡值,我还很年轻很健康,你咋能误判呢?问题来了,如果你此时访问sp1是不是就会出问题。

此时出现一个严重的问题:此时会导致被拷贝的对象悬空

要求使用者拷贝过后就不能访问被拷贝的对象了,否则就会出现空指针了。

那,咱们也来模拟实现一个auto_ptr呗!!!

auto_ptr.h:

// C++98 管理权转移 auto_ptr
namespace yu
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		// sp2(sp1)
		// sp1 -> sp
		// sp2 -> *this
		auto_ptr(auto_ptr<T>& sp)
		{
			_ptr = sp._ptr;// 管理权转移
			sp._ptr = nullptr;
		}

        auto_ptr<T>& operator=(auto_ptr<T>& sp)
		{
			// 检测是否为自己给自己赋值
			if (this != &sp)
			{
				// 释放当前对象中资源
				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中
				_ptr = sp._ptr;
				sp._ptr = nullptr;
			}
			return *this;
		}

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

		~auto_ptr()
		{
			cout << _ptr << endl;
			delete _ptr;
		}
	private:
		T* _ptr;
	};
}

main函数:

#include "auto_ptr.h"
int main()
{
    yu::auto_ptr<int> sp1(new int);
    yu::auto_ptr<int> sp2(sp1);//管理权转移

    *sp2 = 10;
    cout << *sp2 << endl;
    *sp1 = 10;//sp1悬空
    cout << *sp1 << endl;
    return 0;
}

3.4 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr。

unique_ptr文档

那要怎么做到禁止拷贝呢?如果是c++98,我们可以将拷贝构造函数只声明不定义或者设置成私有,c++11呢?直接使用delete关键字,那咱也模拟实现一下

unique_ptr.h

// 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
// 原理:简单粗暴 -- 防拷贝
namespace yu
{
	template<class T>
	class 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>&sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
	private:
		T* _ptr;
	};
}

3.5 std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

std::shared_ptr文档

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。

1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。

2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。

3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源

4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

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

在库里面,我们还提供了查看引用计数的函数

那我们现在就来模拟实现一个啦

我们把count定义成这样可行吗?这样是不行的,这样写每一个对象都存在着一个count,count是属于每一个对象的,各自独立,你count--,关我count有什么影响!那设计成静态的可以吗?

静态成员变量属于整个类,不属于某一个对象,感觉上好像没有什么问题,我们来模拟实现一下。

namespace yu
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
		{
			count = 1;
		}

		// sp2(sp1)
		shared_ptr(shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			++count;
		}
		int use_count()
		{
			return count;
		}

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

		~shared_ptr()
		{
			count--;
			if (count == 0)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
			}
				
		}
	private:
		T* _ptr;
		static int count;//类里声明
	};
	template<class T>
	int shared_ptr<T>::count = 0;//类外定义
}

然后我们来运行一下

嗯!管理的很好,支持自由拷贝,也没有出现任何问题。

嗯!咱们俩管理着同一块空间,非常好!事出反常必有妖,我们来看下面的场景,先看库里面的结果

随后我们再来使用我们自己模拟实现的shared_ptr看看结果。

此时问题就显示出来了,此时sp1的引用计数为什么变成1了,我们库里面的是6,而且此时只释放了一份资源,这就说明此时使用静态也会出问题,那么导致问题出现的原因是什么呢?

  1. 由于count是静态成员变量,它是属于整个类所有对象的
  2. 当创建sp7对象的时候,此时调用构造函数,会将静态成员变量的count赋值为1
  3. 而对于sp7对象自己,此时count为1没有任何问题,但是对与sp1对象的引用计数就会被修改
  4. 同时还存在一个问题
  5. 创建sp7对象后,释放资源时,由于count被修改为1,执行析构函数的时候,此时sp7调用析构函数,count--为0,会释放sp7资源,然后sp6执行析构函数,count--为-1,不执行析构函数,sp5也是如此,最后就会导致sp1资源没释放,导致内存泄漏。

我们这里的需求是每个资源配一个引用计数,而不是全部是一个引用计数

那咱们来修改一下代码吧

namespace yu
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{
			//构造说明此时是一个新的资源
		}

		// sp2(sp1)
		shared_ptr(shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			// 拷贝时计数器加一
			++(*_pcount);
		}
		int use_count()
		{
			return *_pcount;
		}

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

		~shared_ptr()
		{
			// 析构时计数器减一,减到0说明最后一个管理对象析构了,可以释放资源
			--(*_pcount);
			if (*_pcount == 0)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}
	private:
		T* _ptr;
		int* _pcount;
	};
}

此时我们再来验证一下结果

我们再来看下面的情况,为什么错呢?

因为我们的赋值还没写,此时是浅拷贝,单纯的值拷贝,所以我们再来把赋值写一写

写成下面的形式代码有问题吗?

调式看结果:

那我们就在赋值之前,首先将sp1的计数器减减。

但是如果我们只有sp1呢?没有sp2,那么此时sp1的计数器减完就是0了,然后sp3赋值黑sp1,那么原来sp1的空间呢?那不又内存泄漏了,所以我们要判断一下,如果计数器减到0,说明此时没有人管理这个资源了,我们就要把它释放掉。

来,我们测试一下

那还有没有问题呢?自己赋值给自己就会出现大坑

所以我们就还要修改一下

还有没有问题呢?如果是下面的场景呢?

此时虽然不会出问题,但是我们刚刚的哪个判断就没意义了,里面依然会走一遍,我们期望如果管理的是同一份资源进行赋值,此时直接返回就行。

好啦,完整代码展示

#pragma once

namespace yu
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{
			//构造说明此时是一个新的资源
		}

		// sp2(sp1)
		shared_ptr(shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			// 拷贝时计数器加一
			++(*_pcount);
		}

		// sp1 = sp3;
		// sp1 -> this
		// sp3 -> sp
		// sp1.operator(sp3)
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)//自己给自己赋值
			{
				if (--(*_pcount) == 0)
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}

			return *this;
		}

		int use_count()
		{
			return *_pcount;
		}

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

		~shared_ptr()
		{
			// 析构时计数器减一,减到0说明最后一个管理对象析构了,可以释放资源
			--(*_pcount);
			if (*_pcount == 0)
			{
				cout << "delete: " << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}
	private:
		T* _ptr;
		int* _pcount;
	};
}

看上去shared_ptr解决了我们上面的问题,但是它也有一个缺陷:std::shared_ptr的循环引用

我们先来看看之前我们是怎么释放资源的

struct ListNode
{
	int _val;
	struct ListNode* _next;
	struct ListNode* _prev;

	ListNode(int val = 0)
		:_val(val)
		,_next(nullptr)
		,_prev(nullptr)
	{}

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

int main()
{
	ListNode* n1 = new ListNode(10);
	ListNode* n2 = new ListNode(20);

	delete n1;
	delete n2;

	return 0;
}

通过析构函数 + delete来释放我们的空间

但是我们现在学习了智能指针,它是可以代管资源,那我们是不是可以让智能指针来帮我们释放资源呢?

可以,此时资源成功释放了,very good!!!这里有一点我们需要提示一下。

释放的_ptr是传入的申请的n1节点的空间,delete会调用析构函数 + 释放空间,这里就会调用ListNode的析构函数,从而让我们看到节点被释放。随后我们想节点进行链接。

怎么解决呢?我们可以把_next和_prev都修改成智能指针

此时就没有问题了。

为什么呢?

但是我们再加一句就出现问题,出现循环引用,导致n1和n2节点都没有释放,造成内存泄漏。

图解:

总结一下出现循环引用的原因:左边空间释放->_prev析构->右边空间释放->_next析构->左边空间释放,成环导致无法释放空间。

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了

struct ListNode
{
	int _val;
    // 使用无参构造函数初始化
    weak_ptr<ListNode> _next;
    weak_ptr<ListNode> _prev;

    //不支持指针构造
	ListNode(int val = 0)
		:_val(val)
	{}

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

int main()
{
    // 库里面构造函数加上explicit 
    // 不支持单参数的隐式类型转换
	shared_ptr<ListNode> node1(new ListNode(10));
    shared_ptr<ListNode> node2(new ListNode(20));

    // 此时类型不一样,无法链接
    node1.operator->()->_next = node2;
    node2.operator->()->_prev = node1;

	return 0;
}

我们来运行一下:

为啥weak_ptr可以做到呢?原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和 _prev不会增加node1和node2的引用计数。

那话不多说,我们看一下我们自己的shared_ptr会不会出现上面的问题

现在我们来加上weak_ptr

namespace yu{
	template<class T>
	class shared_ptr
	{
	public:
		// RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			// 拷贝时++计数
			++(*_pcount);
		}

		// sp1 = sp4
		// sp4 = sp4;
		// sp1 = sp2;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;

				// 拷贝时++计数
				++(*_pcount);
			}

			return *this;
		}

		void release()
		{
			// 说明最后一个管理对象析构了,可以释放资源了
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}

		~shared_ptr()
		{
			// 析构时,--计数,计数减到0,
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

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

		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
	};

	// 不支持RAII,不参与资源管理
	template<class T>
	class weak_ptr
	{
	public:
		//weak_ptr(T* ptr){}//不支持
		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*()
		{
			return *_ptr;
		}

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

	private:
		T* _ptr;
	};
}

如果我们想管理多个资源呢?

如果我们想管理多个资源呢?就不能使用我们的shared_ptr来管理多个资源,boost库里面存在shared_array可以解决,但是我们还要去下载这个库,c++是没有提供这个库的,或者c++库也对这个场景进行了单独处理

但是我们期待使用一个通用的方式,使用定值删除器解决多个资源的释放。同时我们还存在一种场景,如果不是new出来的对象(fopen出来的FILE指针我们只需要关闭fclose,不需要释放)如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这 个问题(ps:删除器这个问题我们了解一下)

那咱们的智能指针也改一下呗,我们看到库里面的是在构造函数那里传入可调用对象,所以我们构造函数也要修改,本质是在析构函数那里使用对象对资源进行删除。

但是我们上面可以,但是和库就不一样了,库里面就只有一个模板参数,我们可以使用包装器进行封装,因为我们知道被调用函数的返回类型和被调用函数的形参

shared_ptr.h

#include <functional>

namespace yu
{
	template<class T>
	class shared_ptr
	{
	public:

		function<void(T*)> _del;
		// RAII
		template <class D>
		shared_ptr(T* ptr, D del) //关键是把del给析构函数
			: _ptr(ptr)
			, _pcount(new int(1))
			, _del(del)
		{}

        shared_ptr(T* ptr = nullptr) //此时_del为空
			: _ptr(ptr)
			, _pcount(new int(1))
		{}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			// 拷贝时++计数
			++(*_pcount);
		}

		// sp1 = sp4
		// sp4 = sp4;
		// sp1 = sp2;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;

				// 拷贝时++计数
				++(*_pcount);
			}

			return *this;
		}

		void release()
		{
			// 说明最后一个管理对象析构了,可以释放资源了
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				//delete _ptr;
				_del(_ptr);//借助可调用对象进行删除资源
				delete _pcount;
			}
		}

		~shared_ptr()
		{
			// 析构时,--计数,计数减到0,
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

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

		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
	};

	// 不支持RAII,不参与资源管理
	template<class T>
	class weak_ptr
	{
	public:
		//weak_ptr(T* ptr){}//不支持
		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*()
		{
			return *_ptr;
		}

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

	private:
		T* _ptr;
	};
}

main.c

struct ListNode
{
	int _val;
    // 使用无参构造函数初始化
    yu::weak_ptr<ListNode> _next;
    yu::weak_ptr<ListNode> _prev;

    //不支持指针构造
	ListNode(int val = 0)
		:_val(val)
	{}

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

// 定值删除器
template<class T>
struct DeleteArray
{
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};
int main()
{
    // 管理单个资源
    yu::shared_ptr<ListNode> node1(new ListNode(10),[](ListNode* ptr) { delete ptr; });

    // 管理多个资源
    // DeleteArray是一个仿函数,通过传入匿名仿函数对象即可解决多个资源
    yu::shared_ptr<ListNode> node2(new ListNode[3]{1,2,3},DeleteArray<ListNode>());

    // 管理没有new出来的对象,通过lambda表达式传入可调用对象
    yu::shared_ptr<FILE> p1(fopen("92.cpp", "r"), [](FILE* ptr) {fclose(ptr); });

    return 0;
}

我们来看一下结果:

但是我们使用库的时候,我们单个资源可以不用传可调用对象,所以还要改,但是一句话就可以,包装器给上默认值

function<void(T*)> _del = [](ListNode* ptr) { delete ptr; };//默认直接delete

4.C++11和boost中智能指针的关系

  • 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中的实现的。

总结:

​​​​​​​

  • 45
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值