C++之智能指针

目录

一、为什么需要智能指针

二、内存泄漏

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

2.2、两种内存泄漏

2.3、如何检测内存泄漏

2.4、如何避免内存泄漏

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

3.1、RAII

3.2、std::auto_ptr

3.3、std::unique_ptr

3.4、std::shared_ptr

3.5、智能指针原理

四、C++11和boost中智能指针的关系


一、为什么需要智能指针

下面我们先分析一下下面这段程序有没有什么内存方面的问题?

代码:

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

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	int* p1 = new int;

	// 2、如果p2这里new 抛异常会如何?
	int* p2 = new int;

	// 3、如果div调用这里又会抛异常会如何?
	cout << div() << endl;
	delete p1;
	delete p2;
}

int main()
{
	try

	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

解释:如果是p1在new时抛异常,程序还没有申请到任何资源,所以不需要释放资源,如果是p2在new时抛异常,需要释放资源p1,如果是div调用抛异常,需要释放资源p1和p2。这里不同位置抛异常,释放的资源是不一样的,所以如果由我们自己进行资源管理会比较麻烦,这时我们就可以通过智能指针简化这一过程,智能指针可以自动释放生命周期到了的资源,不需要我们主动释放。

二、内存泄漏

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

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

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

2.2、两种内存泄漏

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

  • 堆内存泄漏(Heap leak)

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

  • 系统资源泄漏

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

2.3、如何检测内存泄漏

在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客

在windows下使用第三方工具:VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库_visual leak detector vs2020-CSDN博客

其他工具:内存泄露检测工具比较 - 默默淡然 - 博客园

2.4、如何避免内存泄漏

  • 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps: 这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要智能指针来管理才有保证。
  • 采用RAII思想或者智能指针来管理资源。
  • 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  • 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

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

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

3.1、RAII

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

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

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

3.2、std::auto_ptr

官网:https://cplusplus.com/reference/memory/auto_ptr/

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

C++之智能指针_第1张图片

解释:在官网中auto_ptr的解释中明确指出从 C++11 开始,此类模板已弃用,并推荐使用unique_ptr来替代该智能指针。这是因为auto_ptr的拷贝构造实现的并不理想,而unique_ptr通过禁止拷贝构造来解决这一问题。如果确实有拷贝需求,后面的shared_ptr可以。

下面演示auto_ptr的使用及问题:

C++之智能指针_第2张图片

从上面代码和结果可以看到auto_ptr是可以进行拷贝构造的,而且拷贝构造后也可以进行正常的释放。那么它的问题在哪里呢?我们看下面一段代码。

C++之智能指针_第3张图片

结果:

C++之智能指针_第4张图片

上图代码中我们只是添加了一行访问ap1智能指针的语句,程序就崩溃了,这就是auto_ptr的问题所在。auto_ptr的拷贝构造并不是真的在拷贝,而是管理权转移,当我们将ap1拷贝给ap2后,ap1就变成了空指针,这时就只能通过ap2来访问这块资源了,而上面代码中我们仍然通过ap1来访问,造成了访问空指针的问题,所以程序崩溃了。下面调试结果我们可以看到ap1被置空。

调试:(此时程序走到ap1->_year++但还未执行)

C++之智能指针_第5张图片

auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理。

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

		auto_ptr(auto_ptr& sp)
			:_ptr(sp._ptr)
		{
			// 管理权转移
			sp._ptr = nullptr;
		}

		auto_ptr& operator=(auto_ptr& 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;
	};
}

3.3、std::unique_ptr

官网:https://cplusplus.com/reference/memory/unique_ptr/

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

基础使用:

template
class DeleteArray
{
public:
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

class Fclose
{
public:
	void operator()(FILE* ptr)
	{
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
	}
};

int main()
{
	// C++11
	unique_ptr up1(new Date);
	// 不支持拷贝
	//unique_ptr up2(up1);

	// 定制删除器
	unique_ptr> up3(new Date[5]);

	//C++11中给出了特化版本
	unique_ptr up4(new Date[5]);

	// 定制删除器
	unique_ptr up5(fopen("test.cpp", "r"));

	return 0;
}

解释:首先unique_ptr禁止了拷贝构造,所以使用该智能指针时无法进行拷贝,其次unique_ptr的底层是用delete释放管理的资源,但是有时我们可能会通过该智能指针申请一个数组进行管理,这时需要delete[ ]释放,或者我们打开了一个文件资源交给该智能指针管理,这是释放资源需要fclose,这些情况下,unique_ptr原有的释放资源的方式就不可用了,所以它的第二个模版参数是一个定制删除器,当我们申请的资源不可以通过delete释放时,我们就可以手动传入一个定制删除器(即传入一个仿函数),它就会通过调用我们传入的定制删除器对资源进行释放,这样就能够实现对资源的合理释放,另外,库里面提供了一个特化版本,当我们需要使用delete[]对资源进行释放时可以使用(即申请了一个数组资源),它只需要在传入第一个模版参数时后面加上一个[ ]即可,具体看上图示例代码。

unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理。

namespace bit
{
	template
	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&sp) = delete;
		unique_ptr& operator=(const unique_ptr&sp) = delete;

	private:
		T* _ptr;
	};
}

3.4、std::shared_ptr

官网:https://cplusplus.com/reference/memory/shared_ptr/

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

  • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  • 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  • 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

示例代码:

class Fclose
{
public:
	void operator()(FILE* ptr)
	{
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
	}
};

int main()
{
	shared_ptr sp1(new Date);
	//支持拷贝
	shared_ptr sp2(sp1);
	shared_ptr sp3(sp2);

	//use_count方法获取引用计数
	cout << sp1.use_count() << endl;

	//shared_ptr也支持特化版本
	shared_ptr sp4(new Date[5]);

	//不能用delete或delete[]释放的资源需要传入定制删除器
	//传法和unique_ptr不一样
	shared_ptr sp5(fopen("test.cpp", "r"), Fclose());

	//通过make_shared创建对象并返回shared_ptr对象
	shared_ptr sp6 = make_shared(2024, 8, 5);

	return 0;
}

解释:首先需要注意的是shared_ptr传入定制删除器的方式和unique_ptr是不一样的,shared_ptr是在形参位置传入,而unique_ptr是在模版参数位置传入。其次需要注意的是make_shared的使用,它使用时需要显示实例化模版参数,之所以有这个方法是因为shared_ptr拷贝构造是通过引用计数实现的,而正常构造出来的智能指针对象需要单独开一块空间来存储引用计数,这样当使用的智能指针多了以后就会有很多零碎的内存用来存储引用计数,造成内存碎片,而make_shared方法可以将存储引用计数这块空间和管理的资源的空间开在一起,这样就可以缓解内存碎片问题。

shared_ptr模拟实现:

#pragma once

#include
using std::function;

namespace bit
{
	template
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}

		template
		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			,_pcount(new int(1))
			,_del(del)
		{}

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

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

				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}

		shared_ptr& operator=(const shared_ptr& sp)
		{
			if (_ptr != sp._ptr)
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}

			return *this;
		}

		~shared_ptr()
		{
			release();
		}

		T* get()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}

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

		function _del = [](T* ptr) {delete ptr; };
	};
}

解释:

首先是引用计数如何实现,我们要确保每个引用计数被指向同一资源的所有share_ptr对象所共享,还要确保指向不同资源的share_ptr对象之间的引用计数相互独立,所以我们采用在堆上开辟空间的方式,构造时直接在堆上开一块空间存储引用计数,拷贝构造时将被管理的资源的指针和指向存储引用计数的空间的指针直接拷贝给新的share_ptr对象,再将计数加一,这样就确保了前面说的问题。

其次,这里实现的share_ptr只实现了拷贝构造没有实现移动构造,这是因为我们实现的shared_ptr并不需要进行深拷贝,即使实现了移动构造交换资源也同样是对两个指针进行赋值,没有意义,移动构造在需要深拷贝的类中才会体现出意义来,所以这里没有实现移动构造。另外,我们还需要解决管理不同资源需要不同的释放方式的问题,这里我们在提供一个构造,该构造增加一个模版参数,允许外界传入实现释放资源功能的仿函数,同时为了解决我们能够在释放资源的函数中使用传入的仿函数的问题,我们直接将仿函数封装为成员变量,这里用function封装一下是为了方便使用,另外我们还需要考虑到外界没有传入仿函数的情况,所以我们给一个缺省值。

最后,赋值重载中,我们不可以将该shared_ptr直接指向形参传入的智能指针指向的资源,因为该shared_ptr可能本身就已经指向了一个资源了,所以我们需要先释放原有资源,再指向新资源,另外还要防止自己赋值给自己的情况,因为当只有一个shared_ptr指向某一个资源时,自己赋值给自己会先释放资源,这时释放后赋值的过程就会出问题。

std::shared_ptr的线程安全问题:

需要注意的是shared_ptr的线程安全分为两方面:

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时 ++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错 乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁 的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

std::shared_ptr的循环引用:

示例代码:

struct ListNode
{
	int _data;

	//前面自己实现的智能指针
	bit::shared_ptr _next;
	bit::shared_ptr _prev;

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

int main()
{
	// 循环引用 -- 内存泄露
	bit::shared_ptr node1(new ListNode);
	bit::shared_ptr node2(new ListNode);

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;

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

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;

	return 0;
}

循环引用分析:

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。同时_prev还指向上 一个节点。
  4. 也就是说_next析构了,node2就释放了;也就是说_prev析构了,node1就释放了。
  5. 但是_next属于node1的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

C++之智能指针_第6张图片

注意:上面节点的引用计数变为0,才会释放,只有当节点要进行释放,才会进行节点里面内容的释放。

如何解决循环引用问题:

C++中提供了weak_ptr来解决循环引用的问题,构造:

C++之智能指针_第7张图片

从构造中可以看出我们可以通过传入一个shared_ptr对象来构造一个weak_ptr。weak_ptr会指向shared_ptr管理的对象。

示例代码一:

struct ListNode
{
	int _data;

	//这里没有使用库里的weak_ptr
	//使用的是自己实现的简易版
	bit::weak_ptr _next;
	bit::weak_ptr _prev;

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

int main()
{
	bit::shared_ptr n1(new ListNode);
	bit::shared_ptr 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;

	// weak_ptr不支持管理资源,不支持RAII
	//std::weak_ptr wp(new ListNode);

	return 0;
}

解释:weak_ptr之所以可以解决循环引用问题是因为weak_ptr指向shared_ptr管理的对象,但是不会增加该对象的循环引用。它只是观察对象的存在,而不拥有对象。

示例代码二:

int main()
{
	std::weak_ptr wp;
	std::shared_ptr sp;

	{
		std::shared_ptr n1(new ListNode);
		wp = n1;
		cout << wp.expired() << endl;
		n1->_data++;
		sp = wp.lock();
	}

	cout << wp.expired() << endl;

	return 0;
}

注意:weak_ptr不支持管理资源,不支持RAII。

3.5、智能指针原理

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

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

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

你可能感兴趣的:(C++,经验分享,笔记,c++,开发语言)