C++ 智能指针

为什么需要智能指针?

首先我们分析以下程序代码:

cpp 复制代码
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	// 1、如果p1这里new 抛异常:直接跳到适配的catch处捕捉异常,导致没有delete p1
	// 2、如果p2这里new 抛异常:直接跳到适配的catch处捕捉异常,导致没有delete p1,delete p2
	// 3、如果div调用这里又会抛异常:直接跳到适配的catch处捕捉异常,导致没有delete p1,delete p2
	//以上均会导致内存泄漏
	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;
}

内存泄漏

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

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

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

内存泄漏分类(了解)

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

堆内存泄漏(Heap leak):

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

系统资源泄漏:

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

如何避免内存泄漏

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

  2. 采用RAII思想或者智能指针来管理资源。(智能指针是RAII思想的一种具体实现)

  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。 4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵

小总结:

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

智能指针的使用及原理

智能指针的相关知识可以归结为以下三种:

1 RAII 2 像指针一样 3 拷贝问题

RAII

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

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

  1. 不需要显式地释放资源

  2. 采用这种方式,对象所需的资源在其生命期内始终保持有效

cpp 复制代码
 //使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
	SmartPtr(T*ptr = nullptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout<< "~SmartPtr()->" << _ptr << endl;
		delete _ptr;//暂时写成这样,下文有完善的解决方法,解决new[],需要使用delete[]的问题
	}

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

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

private:
	T* _ptr;
};

演示抛异常--智能指针能够解决由此带来的内存泄漏问题

cpp 复制代码
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);
	cout << div() << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch(const exception&e)
	{
		cout << e.what() << endl;
	}
	return 0;
}
cpp 复制代码
void TestSmartPtr1()
{
	SmartPtr<int> sp1(new int);
	*sp1 = 1;

	SmartPtr<pair<string, int>> sp2(new pair<string, int>("xxxx", 1));
	sp2->first += 'y';
	sp2->second += 1;
	//实际是sp2.operator->()->second += 1;//语法上为了可读性,省略了一个->
}

int main()
{
	TestSmartPtr1();
    return 0;
}
复制代码
cpp 复制代码
void TestSmartPtr2()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2 = sp1;
}

以上代码程序运行会崩溃

因为sp1管理一份资源,用sp1拷贝构造生成的sp2与sp1共同管理同一份资源,但是sp2析构会释放该资源,sp1析构时又会再次释放该资源,导致程序崩溃,后面介绍的shared_ptr可用解决此问题

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

  1. RAII特性

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

auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针

auto_ptr的实现原理:管理权转移的思想

下面简化模拟实现auto_ptr来了解它的实现原理

cpp 复制代码
namespace djx
{
	// C++98
	// 管理权转移,最后一个拷贝对象管理资源,被拷贝对象都被置空
	// 很多公司都明确规定了,不要用这个
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T*ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout<< "delete->" << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}

		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;
				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

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

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}
cpp 复制代码
void test_auto_ptr1()
{
	djx::auto_ptr<int> ap1(new int);
	djx::auto_ptr<int> ap2 = ap1;
	// 管理权转移,导致对象ap1悬空
	
	*ap2 = 4;
	//cout << *ap1<< endl; //这条语句会报错
	cout << *ap2<< endl;
}


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

unique_ptr

C++11中开始提供更靠谱的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来了解它的原理

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

		~unique_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&up) = delete;
	private:
		T* _ptr;
	};

shared_ptr

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

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源

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

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

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

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

暂时不考虑线程安全版本:

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

		void release()
		{
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
		}

		~shared_ptr()
		{
			release();
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return* this;
		}

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

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

		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
	};

智能指针发展历史(简单版):

C++98 auto_ptr 管理权转移--不好的设计,对象悬空(不建议使用)

boost scoped_ptr 防拷贝--简单粗暴,对于不需要拷贝的场景非常好

boost shared_ptr 引用计数,最后一个释放的对象释放资源--复杂一些,但是支持拷贝,非常完美,但是也有问题:循环引用-->用weak_ptr来解决

C++11 unique_ptr 防拷贝--简单粗暴,对于不需要拷贝的场景非常好

C++ shared_ptr 引用计数,最后一个释放的对象释放资源--复杂一些,但是支持拷贝,非常完美,但是也有问题:循环引用-->用weak_ptr来解决

shared_ptr的循环引用:
cpp 复制代码
struct ListNode
{
	int val;
	djx::shared_ptr<ListNode> next;
	djx::shared_ptr<ListNode> prev;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
cpp 复制代码
void test_shared_ptr2()
{
	djx::shared_ptr<ListNode> n1 = new ListNode;
	djx::shared_ptr<ListNode> n2 = new ListNode;

	//循环引用
	n1->next = n2;
	n2->prev = n1;
}
cpp 复制代码
int main()
{
	test_shared_ptr2();
	return 0;
}

以上代码只要隐去任意一方:n1->next = n2 / n2->prev = n1 资源就可以正确释放

可用使用weak_ptr来解决shared_ptr的循环引用问题,shared_ptr会增加引用计数,但是weak_ptr不会

weak_ptr:

  1. 不是传统智能指针,不支持RAII 2.像指针一样使用 3.不参与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& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}

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

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

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

原理就是,n1->next = n2;和n2->prev = n1;时weak_ptr的next和 prev不会增加n1和n2的引用计数

cpp 复制代码
struct ListNode
{
	int val;
	djx::weak_ptr<ListNode> next;
	djx::weak_ptr<ListNode> prev;

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



void test_shared_ptr2()
{
	djx::shared_ptr<ListNode> n1(new ListNode);
	djx::shared_ptr<ListNode> n2(new ListNode);
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	// 循环引用
	n1->next = n2;
	n2->prev = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
}

shared_ptr****的线程安全问题

考虑线程安全版本的shared_ptr:

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

	shared_ptr(const shared_ptr<T>& sp)//拷贝构造--引用计数要++
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _pmtx(sp._mtx)
	{
		AddRef();
	}

	void AddRef()
	{
		_pmtx->lock();//加锁
		++(*_pcount);
		_pmtx->unlock();//解锁
	}

	void Release()
	{
		_pmtx->lock();
		bool flag = false;
		if (--(*_pcount) == 0 && _ptr)
		{
			delete _ptr;
			delete _pcount;
			flag = true;
		}
		_pmtx->unlock();

		if (flag)
			delete _pmtx;
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			Release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp._pmtx;
			AddRef();
		}
		return *this;
	}

	int use_count()
	{
		return *_pcount;
	}

	~shared_ptr()
	{
		Release();
	}

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

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

private:
	T* _ptr;
	int* _pcount;
	mutex* _pmtx;//锁的指针--指向一个锁(专门用于引用计数++/--操作的锁)
};

shared_ptr智能指针是线程安全的,引用计数的加减是加锁保护的,但是指向的资源不是线程安全的

指向的堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了 ,引用计数的线程安全问题,是智能指针要处理的
shared_ptr的线程安全分 为两方面:

  1. 智能指针对象中引用计数若由是多个智能指针对象 共享 (即多个智能指针管理同一份资源),两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2。这样的话引用计数就不正确,会导致资源未释放或者程序崩溃的问题,故而智能指针中 引用计数++、--是需要加锁 的,也就是说引用计数的操作得保证是线程安全的
    一旦引用计数的++,--操作加锁了,即使某个线程的时间片到了,切走了(这时候可能++/--操作进行到第二步,没有全部完成),但是没有解锁,换成其他线程来运行时,因为上一个线程没有解锁,所以该线程申请不到锁,也就无法进行 引用计数的++,--操作,直到持有锁的那个线程重新运行,完成完整的++/--操作后,释放锁。
    原因在于有以下这种场景:若是线程1的智能指针中的引用计数++(引用计数由1-->2)完成后,但是线程1的时间片到了,那么此时2这个数值还未能写回内存(多个智能指针共享的引用计数仍然为1),保存好自己的上下文数据后,该线程就被切走了,换成线程2正在工作,线程2的智能指针中的引用计数++完成(引用计数由1-->2)并且写回内存--共享的引用计数由1真正变成2,线程2的时间片到了,被切走,换成线程1来继续完成之前未完成的工作:将2这个数值写回内存

    可以看到++操作的汇编代码分成三步进行

  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题

定制删除器

如果不是 new 出来的对象如何通过智能指针管理呢?其实 shared_ptr 设计了一个删除器来解决这
个问题

new空间常见的有两种方式:new单个空间,new连续空间(即new [])

delete空间常见的有两种方式:delete单个空间,delete连续空间(即delete [])

在上文的用智能指针来管理资源的代码中,我们考虑的暂时只是new单个空间的情况,所以释放空间时,使用的是delete,如果我们要使用智能指针管理的资源是一块连续的空间呢?自然释放该资源需要delete[],那么该怎么设计智能指针,既能管理好单个资源空间的释放,又能管理好一块连续的资源空间的释放,即怎么做到在设计智能指针类的时候,析构函数即能delete又能delete[]?

答案:使用定制删除器

如果智能指针管理的是一块连续的资源空间,那么需要我们自己传递函数指针/仿函数/lambda表达式(它们的功能设计成delete[],来释放这块连续的资源空间),如果智能指针管理的是单个资源空间,那么我们正常用智能指针管理该资源即可

cpp 复制代码
	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)
		{}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				_del(_ptr);
				delete _pcount;
			}
		}

		~shared_ptr()
		{
			release();
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return* this;
		}

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

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

		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };
//可接收函数指针/仿函数/lambda表达式,该类的析构函数通过它们去释放连续的资源空间
		//给定缺省值:shared_ptr管理单个资源空间时无需传入函数指针/仿函数/lambda表达式,默认释放使用delete
	};
cpp 复制代码
template<class T>
struct DelArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

int main()
{
	// 定制删除器
	djx::shared_ptr<ListNode> sp1 (new ListNode[10], DelArray<ListNode>());//智能指针管理连续的资源空间
	djx::shared_ptr<ListNode> sp2(new ListNode[10], [](ListNode* ptr) {delete[] ptr; });
	djx::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });

	djx::shared_ptr<ListNode> sp4(new ListNode);//智能指针管理单个资源空间
	return 0;
}
相关推荐
ROC_bird..41 分钟前
STL - vector的使用和模拟实现
开发语言·c++
机器视觉知识推荐、就业指导41 分钟前
C++中的栈(Stack)和堆(Heap)
c++
Mr_Xuhhh3 小时前
递归搜索与回溯算法
c语言·开发语言·c++·算法·github
无敌岩雀3 小时前
C++设计模式行为模式———命令模式
c++·设计模式·命令模式
爱吃生蚝的于勒5 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
小白学大数据7 小时前
Python爬虫开发中的分析与方案制定
开发语言·c++·爬虫·python
versatile_zpc9 小时前
C++初阶:类和对象(上)
开发语言·c++
小鱼仙官9 小时前
MFC IDC_STATIC控件嵌入一个DIALOG界面
c++·mfc
神仙别闹9 小时前
基本MFC类框架的俄罗斯方块游戏
c++·游戏·mfc
娅娅梨10 小时前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++