首页 > 其他分享 >智能指针

智能指针

时间:2023-06-08 19:05:28浏览次数:47  
标签:泄漏 auto 智能 内存 shared ptr 指针

       今天主要是总结下我刚学习的智能指针,分为四方面进行智能指针的整理。注意:本次使用的编译语言为C++,编译器为VS2013。下面直接开始今天的正题吧

为什么要使用智能指针?

首先我们先来看一段代码:

int main() {
	try {
		int* p = new int[10];// 动态分配内存失败
		throw std::exception();//获取异常
		delete[] p;// 在异常抛出前的代码中进行内存释放
	}
	catch (...) {
		std::cout << "Exception caught!" << std::endl;
	}
	_CrtDumpMemoryLeaks();//VS提供的一种函数,用于检测内存泄漏并输出信息
	return 0;
}

此时,当我们查看编译器输出时,会出现以下提示:

智能指针_智能指针

       这就说明,我们的程序出现了内存泄漏。除了上述异常中断内存释放,我们在写一些大型的项目时,也会出现遗漏释放new开辟的空间,new和delete不匹配等原因,导致内存泄漏或野指针的情况。

内存泄漏

在讲解智能指针之前,我们需要了解什么是内存泄漏?它的危害又是什么?

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

简单来说,就是内存没有正确的释放,导致内存空间一直被占据,无法在被程序使用,从而造成内存资源浪费的情况。

       内存泄漏会导致系统运行速度变慢,甚至会导致系统崩溃。在长时间运行的程序中,内存泄漏问题会越来越严重,最终可能导致系统无法正常运行,需要重启系统或者重新启动程序。因此,内存泄漏是一个非常严重的问题,需要及时检测和解决。因此我们在运行程序中需要警惕,避免出现内存泄漏。针对内存泄漏,C++也提供了一种指针可以帮助我们有效的解决内存泄漏的问题,那么就是智能指针。

智能指针

       智能指针是一种在C++语言中用来管理动态内存的工具。它可以自动管理内存的生命周期,避免一些常见的内存管理错误,如内存泄漏和野指针等问题。智能指针实际上是一个封装了指针的类,它重载了指针相关的运算符,并提供了一些额外的功能,如自动内存分配、自动释放、引用计数等。

        C++11标准库中提供了两种常用的智能指针:unique_ptr和shared_ptr,但是在这之前,其实还有一种智能指针,虽然我们不会使用它,但我们还是可以先了解一下该指针,再去了解现在所使用的指针。

auto_ptr

       auto_ptr是 C++98 标准库中的一个智能指针,它允许程序员管理动态分配的对象,同时避免内存泄漏和空指针引用等问题。

       auto_ptr的主要特点是在析构时自动释放它所管理的对象,从而避免手动释放内存的麻烦。同时,拷贝构造函数和赋值运算符重载会将原先的指针置为nullptr,从而避免悬空指针的问题。

我们来看一个具体的缺陷:

int main() {
    std::auto_ptr<int> p1(new int(42));
    std::auto_ptr<int> p2(p1);
    std::cout << *p1 << std::endl;// 输出结果不确定,可能会崩溃
    return 0;
}

       该问题主要是,p1将自己资源的所有权都交给了p2,此时的p1就变为了野指针,当我们在访问p1时,程序就可能崩溃或者输出不确定的结果。

以下是auto_ptr指针的简单实现:

template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _owner(false)
		{
			if (_ptr)
			{
				_owner = true;
			}
		}
		~auto_ptr()
		{
			if (_ptr&&_owner)
			{
				delete _ptr;
				_ptr = nullptr;
				_owner = false;
			}
		}
		//具有指针类似的行为
		T* operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//资源转移
		auto_ptr(auto_ptr<T>& ap)//拷贝构造
			:_ptr(ap._ptr)
			, _owner(ap._owner)
		{
			ap._owner = false;
      delete ap._ptr;
		}
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				if (_ptr&&_owner)//查看当前指针所在位置是否有资源,有需要删除
				{
					delete _ptr;
				}
				_ptr = ap._ptr;//接手别人的资源
				_owner = ap._owner;
				ap._owner = false;
			}
			return *this;
		}
	private:
		T* _ptr;
		bool _owner;//资源释放的权限
	};

由于设计的缺陷,C++11标准中已经弃用了该指针,并在C++17标准中被完全移除了。现在我们更推荐使用unique_ptr和shared_ptr指针。

unique_ptr

       unique_ptr是C++11标准中的一种智能指针,其主要特点就是拥有资源的独占所有权,即同一时间,只能有一个unique_ptr指针指向同一个对象,当unique_ptr被销毁时,它锁管理的对象也会被销毁。这样也就可以避免犯auto_ptr的错误,更好的避免内存泄漏等问题。

       简单来说,unqiue_ptr指针就是防止一个指针拷贝另一个指针,因此,在封装该指针时,就会禁止生成拷贝构造函数或者赋值运算符重载。

       在C++11中,防止生成某种函数的方法就是在函数后面加上=delete,我们来看具体实现和结果:

智能指针_auto_ptr_02

此时,运行程序:

智能指针_shared_ptr_03

此时编译器就会告诉我们,引用的是一个已删除的函数。

       其次,我们还可以将拷贝构造函数和赋值运算符重载权限设置为私有的,那么我们在外部也不能调用该函数,实现上述效果。

以下是unqiue_ptr指针的简单实现:

template<class T>
	class DFDef//删除器
	{
	public:
		void operator()(T*& ptr)
		{
			if (ptr)
			{
				delete ptr;
				ptr = nullptr;
			}
		}
	};
	template<class T, class DF = DFDef<T>>//默认处理方式
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				DF()(_ptr);
				_ptr = nullptr;
			}
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T* Get()
		{
			return _ptr;
		}
		//C++11中实现防拷贝
		unique_ptr(const unique_ptr<T>&) = delete;
		unique_ptr<T> operator=(const unique_ptr<T>&) = delete;
	private:
		T* _ptr;
	};

       该指针的实现相对简单,主要是通过=delete来限制拷贝构造函数和赋值运算符重载自动生成。但是,对于大型项目而言,unique_ptr指针还是存在它的缺陷,无法实现资源的共享;并且,由于无法实现资源共享,那么在移动对象时,也需要提供移动构造函数和移动运算符重载,大大增加代码的复杂程度,因此C++11还提供了一种智能指针来解决上述问题。

shared_ptr

       shared_ptr是C++11标准库中的智能指针,用于管理动态分配的内存。其特点在于可以自动进行内存的引用计数和释放,实现内存的共享,从而避免内存泄漏和悬挂指针等问题,提高代码的安全性和可维护性。

       对于shared_ptr的用法,就和上述的两种智能指针用法类似,那么接下来我们来看下底层原理。对于shared_ptr的计数器,很多人想到的都是static修饰的静态成员变量,但是在这里静态成员变量却不能满足我们的要求。我们来看个示例:

#include<memory>
//构建一个static修饰的计数器
namespace lzx
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* a = nullptr)
		:_a(a)
		{
			if (_a)
			{
				_b = 1;
			}
		}
		~shared_ptr()
		{
			if (_a && 0 == --_b)
			{
				delete _a;
				_a = nullptr;
			}
		}
		T& operator*()
		{
			return *_a;
		}
		T* operator->()
		{
			return _a;
		}
		shared_ptr(shared_ptr<T>& sp)
			:_a(sp._a)
		{
			_b++;
		}
	private:
		int* _a;
		static int _b;//计数器
	};
	template<class T>
	int shared_ptr<T>::_b = 0;
}
void Test()
{
	lzx::shared_ptr<int> a(new int(10));
	lzx::shared_ptr<int> b(a);//此时计数器变为2

	lzx::shared_ptr<int> c(new int(10));
}
int main()
{
	Test();
	_CrtDumpMemoryLeaks();//检测是否有数据丢失
	return 0;
}

       这里我来解释下代码的作用,class类封装了一个static修饰的计数器,当有指针指向同一对象时,计数器++。从测试Test()函数可以看出,a和b指向同一地址空间,c单独指向一个地址空间;那么按照我们的想法,资源是正常释放的,但是,当我们运行时就会发现,有4字节的资源泄漏了:

智能指针_unique_ptr_04

这是为什么呢??

       我们进一步调试代码,发现当对象c创建后,a和c的计数器都会++,也就是说这个const修饰的计数器作用域类的全部对象,只要有对象被创建,不管是不是指向同一空间,计数器都会++,这也就导致了之后的a,b对象内存并没有被释放,从而造成内存泄漏。

因此,我们这里不能使用static修饰的静态成员变量作为计数器,会有很大错误!!这里,我们可以使用一个指针指向一块空间来充当计数器,这样,在赋值的过程中,只有指向相同资源的对象才会指向相同的计数器,下面我们来简单实现以下shared_ptr指针:

	template<class T>
	class DFDef//删除器
	{
	public:
		void operator()(T*& ptr)
		{
			if (ptr)
			{
				delete ptr;
				ptr = nullptr;
			}
		}
	};
	template<class T,class DF=DFDef<T>>//默认删除方式
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(nullptr)
			, _pMutex(nullptr)
		{
			if (_ptr)
			{
				_pcount = new int(1);
				_pMutex = new mutex();
			}
		}
		~shared_ptr()
		{
			Release();
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//解决浅拷贝问题:引用计数
		shared_ptr(const shared_ptr<T>& sp)//拷贝构造函数
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pMutex(sp._pMutex)
		{
			AddRef();
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)//赋值运算赋重载
		{
			if (this != &sp)
			{
				Release();
				//共享资源和计数++
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pMutex = sp._pMutex;
				++(*_pcount);
			}
		}
	private:
		void AddRef()//指向对象指针+1
		{
			_pMutex->lock();
			++(*_pcount);
			_pMutex->unlock();
		}
		void Release()//释放对象
		{
			bool flag = false;
			_pMutex->lock();//保证原子操作
			if (_ptr && 0 == --(*_pcount))//若减完计数器为0,则释放对象
			{
				DF()(_ptr);
				delete _pcount;
				_pcount = nullptr;
				flag = true;
			}
			_pMutex->unlock();
			if (flag)//若为最后一个指针,释放锁空间
			{
				delete _pMutex;
				_pMutex = nullptr;
			}
		}
	private:
		T* _ptr;
		int* _pcount;//计数必须为静态成员变量
		mutex* _pMutex;//锁机制
	};

这里我给大家画个图更好理解代码:

智能指针_shared_ptr_05

这样也就实现了在相同类中,不同对象分别计数的功能,之后也就不会出现上述内存泄漏的问题。

相对的,shared_ptr指针也有它的缺陷:

  1. 引用计数器需要动态开辟内存空间,增加了内存开销和运行时的效率
  2. 引用计数的方式可能会导致循环引用问题,如果两个对象互相持有 ,它们的引用计数永远不会变为 0,从而导致内存泄漏。
  3. 不能管理动态数组,因为它使用而不是来释放内存,可能导致未定义的行为。

因此,使用上述智能指针需要谨慎使用,使用哪种类型的智能指针需要适合它的场景,并不是shared_ptr最好,需要考虑对应的场景。

标签:泄漏,auto,智能,内存,shared,ptr,指针
From: https://blog.51cto.com/u_15209404/6442256

相关文章

  • 直播倒计时1天 | 一体化智能可观测平台如何保障电商节大促
    ......
  • 智能座舱之蓝牙模块测试要点
    智能座舱蓝牙模块是车辆中的一项重要功能,用于提供与移动设备的无线连接,例如手机、音乐播放器等。下面是对智能座舱蓝牙模块测试的详细介绍:1.连接和配对测试测试蓝牙模块的连接性能,验证其能够与各种类型的设备进行配对和连接。验证蓝牙模块的连接稳定性,包括在连接过程中的自......
  • 链表和双指针框架
    链表和双指针框架前后指针:方便链表删除快慢指针:获取链表倒数第N个元素快慢指针+前后指针:组合问题快慢指针:相交、判环、起点、长度双索引指针:合并/分割/拼接链表链表处理细节细节1:创建额外的哨兵节点的时机细节2:链表递归顺序细节3:虚拟节点细节4:递归实现双向遍历 前后指针:方便链......
  • 系统工程(二十七)智能制造体系
    智能制造体系:系统层级:设备层,单元层,车间层,企业层,协同层。(包含组成的关系,先有设备,设备组成单元,单元组成车间,车间组成企业,然后是企业间的协同)智能特征:互联互通、融合共享、系统集成、新型业态。(按层级依赖的关系,先互联互通才能融合共享,为了共享方便再升级为系统集成,最后是各种新型......
  • 李开复谈AI和大语言模型的竞争格局和未来展望:谁将引领人工智能的下一个飞跃?
    原创|文BFT机器人李开复谈AI和大语言模型的竞争格局和未来展望:谁将引领人工智能的下一个飞跃?01AI2.0时代下:壮志凌云,自强不息5月28日,在2023中关村论坛“人工智能大模型发展论坛”上,创新工场董事长兼首席执行官李开复的演讲主要围绕,在AI2.0时代下,中国的大模型发展应该做到壮志凌云......
  • C++ this 指针
    第一部分this指针的类型可理解为Box*。此时得到两个地址分别为box1和box2对象的地址。实例:#include<iostream>usingnamespacestd;classBox{public:Box(){;}~Box(){;}Box*get_address()//得到this的地址{......
  • C++ 指向类的指针
    C++指向类的指针一个指向C++类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符->,就像访问指向结构的指针一样。与所有的指针一样,您必须在使用指针之前,对指针进行初始化。下面的实例有助于更好地理解指向类的指针的概念:#include<iostream>usin......
  • 人工智能
    #include<bits/stdc++.h>usingnamespacestd;intmain(){stringa;cout<<"请想一个数(1~100)"<<endl;system("pause");intmax=100,min=0;while(a!="对了"){cout<<"我猜那个数是:&......
  • 关于安科瑞智能通讯管理机在能源计量行业的应用-安科瑞张田田
    建筑能耗应用场景描述在电力监控系统能源计量中经常会运用到通信管理机,通信管理机也称作DPU,其具有多个下行通讯接口及一个或者多个上行网络接口,相当于前置机,即监控计算机,用于将一个变电所内所有的智能监控/保护装置的通讯数据整理汇总后,实时传送至上级主站系统(后台监控中心系统和DCS......
  • 直播预告 | 一体化智能可观测平台如何保障电商节大促
    ......