C++11:智能指针

1.为什么需要智能指针

cpp 复制代码
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");//标准库中定义的一个异常类
	return a / b;
}

void func()
{
	 //1、如果p1这里new 抛异常会如何?
	 //2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = new int;

	cout << div() << endl;
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		func();
	}
	catch (const std::exception&e)//异常可以被基类的引用和指针捕获
	{
		cout << e.what() << endl;//what() 是 std::exception 类的一个公有虚成员函数,用于获取异常的描述信息
	}
	return 0;
}

问题分析:上面的问题分析出来我们发现有什么问题?

如果p1抛异常无影响,p2抛异常就无法释放p1,div抛异常无法释放p1,p2,因此catch异常时要分很多种情况。如果申请的空间更多,那情况会更复杂。

2.内存泄漏

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

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

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

2.2 内存泄漏分类

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

  • 堆内存泄漏(Heap leak):堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
  • 系统资源泄漏:指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

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

3.1 RAII

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

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

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

智能指针时 RAII 思想的一种实现, 例如使用RAII思想设计的SmartPtr类

cpp 复制代码
template <typename T>
class SmartPtr {
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{
	}

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

private:
	T* _ptr;
};

void Func()
{ 
	SmartPtr<int> sp1(new int[10]);//抛异常和不抛这里都可以通过析构释放空间,
	//可以防止发生异常后,后续代码不执行
	SmartPtr<char> sp2(new char[10]);

	cout << div() << endl;
}

通过构造函数和析构函数我们可以实现资源自动的释放,然而我们也需要 operator* 和 operator->来实现指针的功能:

cpp 复制代码
T& operator*()//解引用
{
    return *_ptr;
}

T* operator->()//解引用
{
    return _ptr; //(* ).   .运算符前是对象,   ->运算符前是指针,返回对象成员变量
}

3.2 引用计数实现指针拷贝

我们需要多个指针共同管理一块空间,我们需要进行指针拷贝。

C++98版本的库中就提供了auto_ptr的智能指针。auto_ptr的实现原理:管理权转移的思想,只能有一个指针指向那片空间,一旦拷贝就转移值,原指针制空NULL,这种方法是不方便的。

3.2.1 unique_ptr

unique_ptr 实现原理:简单粗暴的使用 delete 防拷贝,**让编译器生成函数的删除声明,明确禁止对该函数的调用,对于不需要拷贝的情况比较好。**下面简化模拟实现了一份来了解它的原理

cpp 复制代码
namespace xlh{
	template <typename T>
	class unique_ptr {
	public:
		
		//c++98
		// 1.只声明不实现  但类外可以实现,不能禁止
		//unique_ptr(const unique_ptr<T> ptr);
		//unique_ptr<T>& operator=(const unique_ptr<T> ptr);
		//2.限定为私有
		
		//c++11
		//1.delete,不让调用
		unique_ptr(const unique_ptr<T>& ptr) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& ptr) = delete;

	private:
		T* _ptr;
	};
}

3.2.2 shared_ptr

另外C++11中也开始提供更靠谱的并且支持拷贝的shared_ptr,是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

大致实现方式是引用计数要跟着资源走,所以我们需要下面的方法简单实现一下。

为什么不能直接使用静态成员变量呢,因为当多个同一类型对象管理不同空间时,会出现问题。

cpp 复制代码
namespace xlh{
	//可以拷贝的智能指针
	template <typename T>
	class shared_ptr {
	public:
		//RAII
		shared_ptr(T* ptr = NULL)
			:_ptr(ptr),
			_pcount(new int(1))
		{
		}
		~shared_ptr()
		{
			(*_pcount)--;
			if (*_pcount == 0)
			{
				delete _ptr; delete _pcount;
			}
		}
		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()  //(* ).   .运算符前是对象,   ->运算符前是指针,返回对象成员变量
		{
			return _ptr;
		}

		//c++11
		//2.引用计数
		shared_ptr(const shared_ptr<T>& sp)//拷贝构造
			:_ptr(sp._ptr),
			_pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		//1.自己给自己赋值 sp1 = sp1
		//2.wdz::shared_ptr<int> sp3(sp1);   sp3 = sp1 也算自己给自己赋值所以不用 this != &sp 判断
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//防止自己给自己赋值
			if (_pcount != sp._pcount)
			{
				if (--(*_pcount) == 0)//引用计数--
				{//释放原内存
					delete _ptr; delete _pcount;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				(*_pcount)++;
			}

			//if ((*_pcount)--)//引用计数--
			//{
			//	if (_pcount == 0)//释放原内存
			//	{
			//		delete _ptr;delete _pcount;
			//	}
			//}
			//_ptr = sp._ptr;
			//_pcount = sp._pcount;
			//(*_pcount)++;

			return *this;
		}


		T* _ptr;
		int* _pcount;
	};
}

shared_ptr中比较需要注意的是**opeator=**赋值运算符重载,会面临两种情况

  1. 自己给自己赋值 sp1 = sp1
  2. 使用wdz::shared_ptr<int> sp3(sp1)构造,然后 sp3 = sp1 ,虽然是不同对象,但也算自己给自己赋值,所以不用 this != &sp 判断,我们可以用 _ptr 判断,或者 _pcount 判断。我这里是使用的_ptr。

3.2.3 循环引用使用weak_ptr

当然shared_ptr也有线程安全的问题,我们后面文章讲解。

循环引用会导致内存泄漏,那么什么是循环引用呢?我们执行下面代码会出现问题:

cpp 复制代码
struct ListNode
{
	int val;
	wdz::shared_ptr<ListNode> prev;
	wdz::shared_ptr<ListNode> next;
};

void Test_shared_ptr3()
{
	wdz::shared_ptr<ListNode> n1(new ListNode);
	wdz::shared_ptr<ListNode> n2(new ListNode);
	//循环引用
	n1->next = n2;
	n2->prev = n1;
}

这是所使用的资源分布图那么什么时候会出问题呢,当进行资源释放时会出现问题:

  1. 执行完Test_shared_ptr3()后会释放 n2
  2. 释放n2 需要先释放 n2->_ptr
  3. 释放n2->_ptr 需要先释放 n2->_ptr->prev->_ptr
  4. 释放n2->_ptr->prev->_ptr 需要先释放 n1->_ptr
  5. 释放n1->_ptr 需要先释放 n2_ptr

一路下来引用计数都不会减少,会陷入循环。

c++11给出了weak_ptr:

cpp 复制代码
struct ListNode
{
	int val;
	std::weak_ptr<ListNode> prev1;//weak_ptr 使用时 不增加 引用计数
	std::weak_ptr<ListNode> next1;
};
void Test_shared_ptr3()
{
	std::shared_ptr<ListNode> n3(new ListNode);
	std::shared_ptr<ListNode> n4(new ListNode);
	cout << n3.use_count() << endl;
	cout<< n4.use_count() << endl;
	n4->prev1 = n3;
	n3->next1 = n4;
	
}

这样使用就不会出现问题,原因是 weak_ptr 指向 shard_ptr 指向的空间时不会增加shared_ptr 的引用计数。

下面时weak_ptr 的简单实现:

cpp 复制代码
	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*()
		{
			return *_ptr;
		}
		T* operator->()  //(* ).   .运算符前是对象,   ->运算符前是指针,返回对象成员变量
		{
			return _ptr;
		}
		
	private:
		T* _ptr;
	};

weak_ptr 不支持RALL,可以向指针一样引用,不参与shared_ptr的引用计数,但其实weak_ptr本身也有引用计数,如下图所示,当shard_ptr 提前释放资源,weak_ptr 不会成为野指针,可以检测 expired 状态,直到 weak_ptr 也变为 0。

3.3 实现 new[]

之前我们自己简单实现的使用new []是会崩溃的,boost 库中提供了scope_ptr 和 scope_array,shared_ptr,shared_array,通过 ptr 和 array 这样来区分.

c++11中使用的是 定制删除器,即传入一个可以调用的对象。

这是标准库的使用方法:

cpp 复制代码
template<class T>
struct DelArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

void Test_shared_ptr5()
{
    //仿函数
	std::shared_ptr<ListNode1> sp1(new ListNode1[10],DelArray<ListNode1>());
	//lambda表达式
    std::shared_ptr<ListNode1> sp2(new ListNode1[10], [](ListNode1* ptr) {delete[] ptr; });
	//这里防止文件打开时,中间抛异常,没有被正常关闭
    std::shared_ptr<FILE> sp3(fopen("Test.cpp","r"), [](FILE* ptr) {fclose(ptr); });
}

我们来模拟实现一下,首先这个类是不能变的,只有一个模板参数。但在构造函数增加了一个模板参数。

上面这种方式是不可以的,因为 release 中没有办法访问到 D , 但可以通过包装器解决:

cpp 复制代码
	private:
		T* _ptr;
		int* _pcount;
		
		function<void(T*)> _del = [](T* ptr) {delete ptr; };//传入指针调用,参数是T*

我们还需要增加一个构造函数

cpp 复制代码
template<class D>
shared_ptr(T* ptr,D del)
	:_ptr(ptr)
	, _pcount(new int(1))
	, _del(del)//包装器,传入一个函数对象,调用时会调用这个函数对象
{
}

以下是一种 shared_ptr 的简单实现:

cpp 复制代码
namespace xlh
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr=nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
			{ }

		template<class D>
		shared_ptr(T* ptr,D del)
			:_ptr(ptr)
			, _pcount(new int(1))
			, _del(del)//包装器,传入一个函数对象,调用时会调用这个函数对象
		{
		}
		
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr),
			_pcount(sp._pcount) {
			(*_pcount)++;
		}
		
		void release()
		{
			if (--(*_pcount) == 0)
			{
				_del(_ptr);
				delete _pcount;
			}
		}


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

		T* operator->()//返回的是成员变量
		{
			return _ptr;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (sp._pcount != _pcount)
			{
				release();
				/*if (--(*_pcount) == 0)
				{
					delete _ptr; delete _pcount;
				}*/
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				*_pcount++;
			}

			return *this;
		}

		~shared_ptr()
		{
			release();
		}

		T* get() 
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
		
		function<void(T*)> _del = [](T* ptr) {delete ptr; };//传入指针调用,参数是T*
	};

下面代码可以执行,为了实现最后一行代码,所以封装器成员需要一个默认值。

cpp 复制代码
void Test2()
{
	xlh::shared_ptr<ListNode> p1(new ListNode[10], DeleteFile<ListNode>());   
	xlh::shared_ptr<ListNode> p2(new ListNode[10], [](ListNode* ptr) {delete[] ptr; });
	xlh::shared_ptr<FILE> p3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });

	xlh::shared_ptr<ListNode> p4(new ListNode);//这里没有传入删除器,
    //所以会调用默认的删除器,默认的删除器会调用delete来释放内存,而不是delete[]

本篇结束!

相关推荐
王老师青少年编程1 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【跳跃与过河问题】:过河问题
c++·算法·贪心·csp·信奥赛·跳跃与过河问题·过河问题
摇滚侠1 小时前
Java 零基础全套视频教程,面向对象(高级),笔记 105-120
java·开发语言·笔记
CN-Dust1 小时前
【C++专题】输出cout例题
开发语言·c++
时空系1 小时前
第6篇:多维数据盒——管理大量数据 python中文编程
开发语言·python·ai编程
charlie1145141911 小时前
嵌入式Linux驱动开发(7) 从虚拟设备到真实硬件 —— LED驱动硬件基础
linux·开发语言·驱动开发·内核·c
小短腿的代码世界2 小时前
QCefView深度解析:Qt应用中嵌入Chromium浏览器的终极方案
开发语言·qt
沉默-_-2 小时前
备战蓝桥杯-哈希
c++·学习·算法·蓝桥杯·哈希算法
Reese_Cool2 小时前
【STL】蓝桥杯/天梯赛终极杀器!10个C++字符串核心技巧,暴力破解高频考点
开发语言·c++·蓝桥杯·stl
曹牧2 小时前
Java Web:DispatcherServlet
java·开发语言·前端