【C++】智能指针的详细讲解

前言:
在我们之前学习异常的时候,讲到过异常安全的问题,会有内存泄露的问题。

  • 内存泄露这个问题对程序员的要求很高,申请的空间就必须要手动释放,不像Java这种语言自带垃圾回收器(gc)。
  • 就算是我们手动释放了空间,也有可能存在内存泄露的问题(异常安全),抛异常时会乱跳,有可能就会导致即使手动释放了,也没会内存泄露。
  • 上节在异常种我们可以通过拦截异常手动释放掉,但是防不胜防并不是所有的都能拦截到,于是C++就引入了智能指针。

内存泄漏是指针丢了还是内存丢了?

答:所有的内存泄露都是指针丢了。。
1.内存还在,进程正常结束,内存也会释放
2.僵尸进程有内存泄露,比较可怕。
3.服务器都是长期运行的。

1 RAII思想:

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

  • 在对象构造时获取资源
  • 在对象析构的时候释放资源。

借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

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

> 注意:RAII只是智能指针的一种思想,切不可说RAII就是智能指针

1.1 拦截异常解决不了的内存泄漏:

在上节中我们讲到了在抛异常时会出现内存泄漏的问题,即使是手动释放掉内存也会出现内存泄漏,原因时抛异常时直接跳到了捕获的地方,所以会泄露。

  • 对于上述问题我们也给出了对应的解决办法,那就是拦截异常的方式
  • 为了避免内存泄漏,我们先将异常拦截下来,先释放掉再将捕获的异常抛出
void func()
{
	int* p1 = new int[10]; //这里亦可能会抛异常
	int* p2 = new int[10]; //这里亦可能会抛异常 -- 这里抛异常,p1就没释放掉
	int* p3 = new int[10]; //这里亦可能会抛异常
	int* p4 = new int[10]; //这里亦可能会抛异常

	try
	{
		div();
	}
	catch (...)
	{
		delete[] p1;
		delete[] p2;
		delete[] p3;
		delete[] p4;

		throw;
	}

	delete[] p1;
	delete[] p2;
	delete[] p3;
	delete[] p4;
}

假设我们每个new出来的空间非常大,我们也不确定到底是哪个new失败了
所以,C++就提供了智能指针

  • 利用对象生命周期的特性:出了作用域之后自动调用对象的析构函数,通过析构函数来释放空间。
  • 无论如何都会正常释放资源,抛异常也好,中间抛异常也好,或者是正常结束,出了作用域就调用对象析构函数。

2. 智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将 、->重载下,才可让其像指针一样去使用*.

  • RAII特性
  • 重载operator*和opertaor->,具有像指针一样的行为
template<class T>
class SmartPtr {
public:
   SmartPtr(T* ptr = nullptr)
   : _ptr(ptr)
   {}
   
   ~SmartPtr()
   {
    if(_ptr)
    delete _ptr;
   }
   
   T& operator*() {return *_ptr;}
   T* operator->() {return _ptr;}
private:
   T* _ptr;
};

3 直接拷贝带来的问题:

智能指针衍生的问题~

智能指针管理资源的方式我们不难理解,但是智能指针的拷贝却是个令人头疼的问题

  1. 我们知道我们只是将指针封装了一层
  2. 如果是简单的只拷贝的话,会出两个指针指向同一块资源
  3. 在释放的时候会发生同一块空间释放多次的问题

智能指针最大的问题:在于拷贝构造问题**(拿一个已经有的,来可瓯北另外一个)**

3.1 auto_ptr:

在这里插入图片描述
核心思想:

  • 管理权转移,被拷贝的对象悬空。
namespace Joker
{
	template<class T>
	class auto_ptr
	{
	public:
		//RAII思想
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

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

		//sp2(sp1) -- 拷贝构造(简直就是神人)
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			//将sp1置空,交给sp2管了,sp1不管了
			sp._ptr = nullptr;
		}
      //ap1=ap2
      auto_ptr<T>& operator=(auto_ptr<T>& ap)
      {
             if(this!=&ap)
             {
                 if(_ptr)
                 {
                   delete _ptr;
                 }
                 _ptr=ap._ptr;
                 ap._ptr=nullptr;
             }
      }

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

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

		T* get()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

思路:

  • auto_ptr支持拷贝,但是方式很挫
  • 拷贝之后直接将原来的指针给置空了
  • 这要是不知情的人使用了原来指针,直接就造成非法访问

3.2 unique_ptr:

我们再来看C++11给的解决办法:
在这里插入图片描述
核心思想:

  • 不让拷贝 / 防拷贝 — 拷贝编译就报错。
namespace Joker
{
	//不能拷贝用unique_ptr
	template<class T>
	class unique_ptr
	{
	public:
		//RAII思想
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}

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

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

		T* get()
		{
			return _ptr;
		}

		//C++98的方式:
		//private:
		//sp2(sp1)
		//1、只声明,不实现(不声明会默认生成一个)
		//2、声明成私有
		//不过还是有问题,在类里面可以调用
		//unique_ptr(const unique_ptr<T>& sp);

		//C++11的方式:防拷贝
		unique_ptr(const unique_ptr<T>& sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

	private:
		T* _ptr;
	};
}

思想:

- unique直接不给拷贝,防止拷贝,但是功能不全
在这里插入图片描述
在这里插入图片描述

3.3 shared_ptr:

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr:
在这里插入图片描述
核心思想:

  • 核心原理就是引用计数,记录几个对象管理这块资源,析构的时候 - -(减减)计数,最后一个析构的对象释放资源。

> 思路:

  • 用到引用计数的方式,拷贝就计数++,析构就计数- -
  • 最后一个析构的对象释放资源

这时,对计数这个变量(count)就有要求了,要求共同管理同一个对象的时候要做到对同一个count ++ 或 - -

  • 直接定义成成员变量(不行,对象内存空间独立)
  • 定义一个static 的成员变量,管理多个资源的时候,就会出问题。(静态成员变量为所有类共享,地址都是一样的)
  • 在堆上开辟一段空间,引用计数放在堆上(int* count)—构造时,给count在堆上申请空间。
private:
   T* _ptr;
   int* _pcount;

shared_ptr的代码实现:

namespace joker
{
     template<class T>
      class shared_ptr
     {
     public:
      //RAII思想
          shared_ptr(T* ptr = nullptr)
          :_ptr(ptr)
          , _pRefCount(new int(1))
          , _pmtx(new mutex)
           {}
           
         //拷贝构造
         shared_ptr(const shared_ptr<T>& sp)
         : _ptr(sp._ptr)
         , _pRefCount(sp._pRefCount)
         , _pmtx(sp._pmtx)
         {
            AddRef();
         }


      //引用计数释放资源专用函数
       void Release()
      {
        _pmtx->lock();
        bool flag = false;
        if (--(*_pRefCount) == 0 && _ptr) %考虑 _ptr为nullptr时,不要delete了。
       {
          cout << "delete:" << _ptr << endl;
          delete _ptr;
          delete _pRefCount;
          flag = true;
      }
      _pmtx->unlock();
     if (flag == true)
      {
        delete _pmtx;
      }
    }

    void AddRef()
    {
      _pmtx->lock();
      ++(*_pRefCount);
      _pmtx->unlock();
    }
    
   //赋值重载
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
   {
      //if (this != &sp)
     if (_ptr != sp._ptr)
    {
      Release();
      _ptr = sp._ptr;
      _pRefCount = sp._pRefCount;
      _pmtx = sp._pmtx;
      AddRef();
      }
      return *this;
    }
    int use_count()
   {
     return *_pRefCount;
   }

//析构函数
    ~shared_ptr()
   {
     Release();
   }

// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx; %多线程使用shared_ptr,会出现线程安全问题
};

注意:shared_ptr会存在多线程安全问题,所以要使用互斥锁。

3.4 循环引用的问题:

我们先来看一段代码:

struct ListNode
{
	Joker::shared_ptr<ListNode> _prev = nullptr;
	Joker::shared_ptr<ListNode> _next = nullptr;

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

int main()
{
	Joker::shared_ptr<ListNode> p1(new ListNode);
	Joker::shared_ptr<ListNode> p2(new ListNode);

	p1->_next = p2;
	p2->_prev = p1;

	return 0;
}

为了更好理解上面的问题,看下图:

在这里插入图片描述

3.5 weak_ptr:

weak_ptr不是常规智能指针,没有RAII,不支持直接管理资源。
weak_ptr主要是接收shared_ptr构造,用来解决shared_ptr的循环引用问题。

在这里插入图片描述

  • 其他的智能指针的构造函数可以传一个指针;
  • weak_ptr构造函数不支持接收指针,不管理资源
  • 它接收一个shared_ptr,可以通过shared_ptr来构造weak_ptr
  • 可以指向一块空间,但是不参与空间的管理

构造函数:
在这里插入图片描述

  • 可以说 weak_ptr是shared_ptr的小弟 — 不是传统的智能指针
  • 专门 用来辅助解决shared_ptr循环引用的问题
// 简化版本的weak_ptr实现
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

struct ListNode
{
	Joker::weak_ptr<ListNode> _prev = nullptr;
	Joker::weak_ptr<ListNode> _next = nullptr;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
% _next 和_prev是weak_ptr时,不参与资源释放管理,可以访问和修改到资源,但是不增加计数,不存在循环引用问题。

int main()
{
	Joker::shared_ptr<ListNode> p1(new ListNode);
	Joker::shared_ptr<ListNode> p2(new ListNode);

	p1->_next = p2;
	p2->_prev = p1;

	return 0;
}

3.6 定制删除器:

通过给智能指针unique_ptr和shared_ptr传递一个可调用对象,来定制析构的具体行为

在这里插入图片描述
构造函数
在这里插入图片描述

注意:
传递给智能指针的定义删除器可调用对象,可以是仿函数,lambda表达式,函数指针等

尾声

看到这里,相信大家对这个C++有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值