当前位置 博文首页 > m1059247324的博客:C++ 内存泄漏和智能指针介绍

    m1059247324的博客:C++ 内存泄漏和智能指针介绍

    作者:[db:作者] 时间:2021-09-17 15:24

    内存泄漏

    什么是内存泄漏 ?

    内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

    泄漏的产生

    • 动态分配的资源未释放或者忘记释放或者异常安全退出
    • 没有正确地清除嵌套的对象指针
    • 在释放对象数组时在delete中没有使用方括号
    • 在类的构造函数和析构函数中没有匹配的调用new和delete函数
    • 指向对象的指针数组不等同于对象数组(数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只是释放了每个指针,但是并没有释放对象的空间,应该通过一个循环,将每个对象释放了,然后再把指针释放了)
    • 缺少拷贝构造函数(因为系统默认的拷贝构造是浅拷贝,会造成同一块内存的二次释放)
    • 缺少重载赋值运算符
    • 没有将基类的析构函数定义为虚函数(当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露)
    • 析构void*,导致没有调用析构函数(delete void* 是不会调用析构函数的)
    • 构造的时候用浅拷贝,释放的时候调用了两侧delete(二次释放)
    • 野指针:指向被释放的或者访问受限内存的指针。(指针变量没有被初始化(如果值不定,可以初始化为 nullptr)指针被free或者delete后,没有置为nullptr, free和delete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为 nullptr,指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针)
    • 关于nonmodifying 运算符重载的常见迷思
      a. 返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针
      b. 返回内部静态对象的引用。
      c. 返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收
      d.解决这一类问题的办法是重载运算符函数的返回值不是类型的引用,二应该是类型的返回值,即不是 int&而是int

    内存泄漏的分类:

    • 常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行时都会导致一块内存泄漏。
    • 偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
    • 一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅有一块内存发生泄漏。
    • 隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

    内存泄漏的危害

    内存泄露最明显最直接的影响就是导致系统中可用的内存越来越少,直到所有的可用内存用完最后导致系统无可用内存而崩溃。
    如果导致泄露的操作是一次性的,或是不经常的,一般问题都不大。在应用退出或系统退出时会清理内存(进程的正常退出都是会自动释放内存的);
    如果导致泄露的操作是经常性的或是循环的,则内存会最终消耗完(或很短时间内)而导致系统崩溃。

    智能指针

    为什么要使用智能指针

    因为 C++ 不像 Java 一样具有垃圾回收机制(GC),但是又需要释放内存,防止内存泄漏。智能指针的作用是管理一个指针,主要用于防止内存写了,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

    1.auto_ptr

    auto_ptr 的大概流程就是使用该指针去代替原生指针管理空间,而后将原生指针置空,这样在 auto_ptr 的生命周期到了以后会去调 auto_ptr 的析构函数从而释放原生指针的内存。

    缺点:因为它会将原生指针置空,而如果不清楚底层原理再去访问原生指针的话会出错,所以已经被放弃使用了。

    	//auto_ptr 的简单模拟
    	
    	template<class T>
    	class auto_ptr
    	{
    	public:
    		auto_ptr(T* ptr)
    			:_ptr(ptr)
    		{}
    
    		auto_ptr(auto_ptr<T>& ap)   //拷贝构造后把原生指针置空
    			:_ptr(ap._ptr)
    		{
    			ap._ptr = nullptr;
    		}
    
    		// ap1 = ap2
    		auto_ptr<T>& operator=(const auto_ptr<T>& ap)
    		{
    			if (this != &ap)
    			{
    				if (_ptr)
    					delete _ptr;
    
    				_ptr = ap._ptr;
    				ap._ptr = nullptr;
    			}
    
    			return *this;
    		}
    
    		~auto_ptr()
    		{
    			if (_ptr)
    			{
    				delete _ptr;
    				_ptr = nullptr;
    			}
    		}
    
    		T& operator*()
    		{
    			return *_ptr;
    		}
    
    		T* operator->()
    		{
    			return _ptr;
    		}
    	private:
    		T* _ptr;
    	};
    

    2.unique_ptr

    unique_pt r实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。同时它还设置了防拷贝,避免出现一个空间释放多次的现象,它对于避免资源泄露(new 后忘记 delete )特别有用。它在创建时自动加锁,在销毁时自动解锁

    缺点: 因为设置了防拷贝,所在在涉及到拷贝的场景下,它无法使用。

    	//unique_ptr 的简单模拟
    	
    	template<class T>
    	class unique_ptr
    	{
    	public:
    		unique_ptr(T* ptr)
    			:_ptr(ptr)
    		{}
    
    		unique_ptr(unique_ptr<T>& up) = delete; //防止拷贝构造
    		unique_ptr<T>& operator=(unique_ptr<T>& up) = delete; //防止赋值
    
    		~unique_ptr()
    		{
    			if (_ptr)
    			{
    				delete _ptr;
    				_ptr = nullptr;
    			}
    		}
    
    		T& operator*()
    		{
    			return *_ptr;
    		}
    
    		T* operator->()
    		{
    			return _ptr;
    		}
    	private:
    		T* _ptr;
    	};
    

    3.shared_ptr

    是线程安全的共享式智能指针,同一块空间由多个指针一同进行管理,它使用计数器来查看该空间被几个指针所共享,每当一个指针指向该空间,计数器 + 1,每当一个指针退出后,计数器 -1,只有在计数器 =0 时才会彻底释放该空间资源。

    
    	//shared_ptr 的简单模拟
    	template<class T>
    	class shared_ptr
    	{
    	public:
    		shared_ptr(T *ptr = nullptr)
    			:_ptr(ptr),_pcount(new int(1)),_mtx(new mutex){}
    		
    		shared_ptr(const shared_ptr<T>& sp)
    			: _ptr(sp._ptr), _pcount(sp._pcount),_mtx(sp._mtx)
    		{
    			plus();
    		}
    
    		//sp1 = sp4,就是让sp1释放原理管理的空间(如果其 pcount == 1),再去和sp4共同管理sp4的空间
    		shared_ptr<T>& operator= (shared_ptr<T>& sp)
    		{
    			if (this != &sp)
    			{
    				release();
    				_ptr = sp._ptr;
    				_pcount = sp._pcount;
    				_mtx = sp._mtx;
    				plus();
    			}
    			return *this;
    		}
    
    		void plus()  //上锁计数器++
    		{
    			_mtx->lock();
    			++(*_pcount);
    			_mtx->unlock();
    		}
    	
    		void release()
    		{
    			bool flag = false;
    
    			_mtx->lock();
    			if (--(*_pcount) == 0)
    			{
    				if (_ptr)
    				{
    					delete _ptr;
    					_ptr = nullptr;
    				}
    				delete _pcount;
    				_pcount = nullptr;
    				flag = true;
    			}
    			_mtx->unlock();
    			if (flag == true)   //连互斥锁也释放
    			{
    				delete _mtx;
    				_mtx = nullptr;
    			}
    		}
    		T* operator->()
    		{
    			return _ptr;
    		}
    
    		T& operator* ()
    		{
    			return *_ptr;
    		}
    		
    		int use_count()
    		{
    			return *_pcount;
    		}
    
    		~shared_ptr()
    		{
    			release();
    		}
    	private:
    		T* _ptr;
    		int* _pcount;
    		mutex* _mtx;  //定义成指针,因为计数器是指针类型的
    	};
    

    缺点:在循环引用的情况下会出错

    循环引用

    在这里插入图片描述
    4. week_ptr

    weak_ptr 是一种不控制对象生命周期的智能指针, 严格来说其实不算智能指针(因为它没有 RAII 管理机制),它是为了解决 shared_ptr 在遇到循环引用时的问题而诞生的 (将链表中的节点改为 week_ptr 即可)。它的构造和析构不会引起引用记数的增加或减少。

    	template<class T>
    	class weak_ptr
    	{
    	public:
    		weak_ptr() = delete; 
    		
    		weak_ptr(const shared_ptr<T>& sp) //不能拷贝原生指针,智能拷贝 shared_ptr 指针
    			:_ptr(sp.get_ptr())
    		{}
    
    		weak_ptr<T>& operator = (const shared_ptr<T>& sp)
    		{
    			_ptr = sp.get_ptr();
    
    			return *this;
    		}
    
    		T& operator*()
    		{
    			return *_ptr;
    		}
    
    		T* operator->()
    		{
    			return _ptr;
    		}
    
    	private:
    		T* _ptr;
    	};
    

    lock_guard

    非智能指针,而是一种 RAII 管理机制,当程序中有共享数据时,你不想让程序其陷入条件竞争,或是出现不变量被破坏的情况,此时可使用std::mutex互斥量来解决数据共享的问题。C++标准库为互斥量提供了一个RAII语法的模板类std::lock_guard,在构造时就能提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁互斥量能被正确解锁。

    lock_guard 和 unique_ptr 的区别

    lock_guard和unique_lock都是RAII机制下的锁,即依靠对象的创建和销毁也就是其生命周期来自动实现一些逻辑,而这两个对象都是在创建时自动加锁,在销毁时自动解锁。所以如果仅仅是依靠对象生命周期实现加解锁的话,两者是相同的,都可以用,因跟生命周期有关,所以有时会用花括号指定其生命周期。但lock_guard的功能仅限于此。unique_lock是对lock_guard的扩展,允许在生命周期内再调用lock和unlock来加解锁以切换锁的状态。

    cs