> 文章列表 > 【C++】智能指针

【C++】智能指针

【C++】智能指针

文章目录

  • 📖 前言
  • 1. 智能指针的引入
    • 1.1 内存泄露的危害:
    • 1.2 异常安全中的内存泄露:
    • 1.3 RAII思想:
    • 1.3 拦截异常解决不了的内存泄漏:
    • 1.4 智能指针解决:
  • 2. 智能指针的拷贝
    • 2.1 直接拷贝的问题:
    • 2.2 auto_ptr:
    • 2.3 unique_ptr:
    • 2.4 shared_ptr:
  • 3. 循环引用
    • 3.1 循环引用的过程:
    • 3.2 weak_ptr:

📖 前言

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

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

异常安全回顾: 传送门


1. 智能指针的引入

1.1 内存泄露的危害:

* 什么是内存泄漏:

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

内存泄漏的危害:

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

1.2 异常安全中的内存泄露:

在上一章节我们在异常安全中,我们提到了,抛异常时会存在内存泄露的问题,即使是手动放了空间,也有可能在抛异常之后还是会导致内存泄露,因为没有执行到释放空间那段代码。

我们当时采用的是拦截异常的方法,但是还是会有防不胜防的情况:


1.3 RAII思想:

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

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

这种做法有两大好处:

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

注意:

  • RAII只是智能指针的一种思想,切不可说RAII就是智能指针

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

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

  • 对于上述问题我们也给出了对应的解决办法,那就是拦截异常的方式
  • 为了避免内存泄漏,我们先将异常拦截下来,先释放掉再将捕获的异常抛出

本以为有了上述方法再也不会出现内存泄露了,直到我遇到了下面的问题:

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失败了

  • 然后下面拦截异常拦截到了,开始手动释放到空间

  • 结果我们也不知道到底是哪个new空间失败了,也不知道该怎么释放空间

  • 很有可能就将没有申请空间的指针给释放掉了

  • 这种情况怎么处理怎么难受。【C++】智能指针

  • 除非我们在try外层现将每个指针置成空,然后在try内部开空间,开空间失败后,下面捕获到对每个指针挨个判空,非空就释放,或者说直接释放(C++对释放空指针没有规定),真的是这样子能把人烦死~~😱

所以此时C++就提供了智能指针~


1.4 智能指针解决:

引入智能指针:

template<class T>
class SmartPtr
{
public://RAII思想SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete" << _ptr << endl;//delete[] _ptr;delete _ptr;_ptr = nullptr;}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* Get(){return _ptr;}
private:T* _ptr;
};
  • 将一个指针封装成一个类,new的资源直接交给智能指针的对象去管理,起到了托管的作用。
  • 借助对象的生命周期(析构函数)管理资源。

有了智能指针之后我们就有了以下的解决办法:

void func()
{//new的资源直接交给对象去管理SmartPtr<int> sp1(new int[10000]);//如果这里抛异常就直接栈帧销毁,出作用域,调用析构函数,后面代码不执行SmartPtr<int> sp2(new int);SmartPtr<int> sp3(new int);SmartPtr<int> sp4(new int);SmartPtr<pair<string, int>> sp5(new pair<string, int>("sort", 1));//借助对象的生命周期(析构函数)管理资源//无论如何都会正常释放资源,抛异常也好,中间抛异常也好,或者是正常结束*sp1 = 0;sp5->second++;
}
  • 利用对象生命周期的特性:出了作用域之后自动调用对象的析构函数,通过析构函数来释放空间。
  • 无论如何都会正常释放资源,抛异常也好,中间抛异常也好,或者是正常结束,出了作用域就调用对象析构函数。

2. 智能指针的拷贝

2.1 直接拷贝的问题:

智能指针衍生的问题~

  • 智能指针管理资源的方式我们不难理解,但是智能指针的拷贝却是个令人头疼的问题
  • 我们知道我们只是将指针封装了一层
  • 如果是简单的只拷贝的话,会出两个指针指向同一块资源
  • 在释放的时候会发生同一块空间释放多次的问题

我们上述实现的智能指针,当一个智能指针赋值给另一个智能指针的时候,是明显的一个浅拷贝(值拷贝),那么在析构的时候就会出现问题:

int main()
{SmartPtr<int> sp1(new int);//当前拷贝会有析构两次的问题SmartPtr<int> sp2(sp1);return 0;
}

在这里插入图片描述
问题出现了,当两个智能指针共同管理同一块空间,在析构的时候就会将同一块空间释放两次~

如何解决当下问题:

  • 有的小伙伴脱口而出的就是 — 深拷贝
  • 真的可以这样嘛??我们先来回一下深拷贝:
    • 深拷贝是将整个资源拷贝一份
    • 然后再用另一个指针来管理这个拷贝出来的的空间

补充:

为什么迭代器可以支持浅拷贝,而不会产生多次释放同一块空间的问题呢?
原因:首先不存在两个迭代器指向同一个空间的情况,其次结点的释放不归迭代器管,而是由容器内部管理释放,综上迭代器就是需要浅拷贝。

那我们的是想让sp2来管理一块新的资源嘛?很显然不是,我们是想让两个智能指针共同管理同一块资源。

那我们能否用静态的指针呢?

  • 假设每一个智能指针内的成员指针是静态的
  • 那么问题来了,虽然这实现了两个指针(实则同一个指针)管理同一个空间
  • 但是如果有其他的空间要管理,用的还是这个指针(因为是静态的),很显然并不妥当
  • 用静态的也解决不了,因为不同对象有不同的空间

2.2 auto_ptr:

我们先来看C++98给的解决办法:

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

  • 管理权转移,被拷贝的对象悬空。

模拟实现:

namespace YY
{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;}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get(){return _ptr;}private:T* _ptr;};
}

思路:

  • auto_ptr支持拷贝,但是方式很挫
  • 拷贝之后直接将原来的指针给置空了
  • 这要是不知情的人使用了原来指针,直接就造成非法访问
  • 每个学C++的人都要来瞻仰一下auto_ptr,然后都要来吐槽一下
  • 很多公司明确要求,不能使用auto_ptr

在这里插入图片描述


2.3 unique_ptr:

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

  • 不让拷贝 / 防拷贝 — 拷贝编译就报错。

模拟实现:

namespace YY
{//不能拷贝用unique_ptrtemplate<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;};
}

因为存在两个指针指向同一个空间造成两次释放的问题,所以我们直接禁止其拷贝 / 赋值

C++98中提供的方式:

*

  • 只声明,不实现(不声明会默认生成一个)— 此时会报一个链接错误
  • 声明成私有(防止有人在类外头将这个函数实现了) — 此时会报编译错误,私有不可访问
  • 不过还是有问题,在类里面可以直接赋值(这个是真的防不住,不过一般类是不会被人修改的)

C++11中提供的方式:

在这里插入图片描述
不生成对应函数的默认版本。

unique直接不给拷贝,没有隐患,但是功能不全


2.4 shared_ptr:

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

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

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

上述几个智能指针都回避掉了指针的值拷贝,但是真的能回避掉吗,总有的场景是需要用到拷贝的。

思路:

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

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

此时我们首先想到的就是将count设置成静态的变量,但是这样是不行的~

在这里插入图片描述
_count竟然减到-2去了。

  • 用静态的话下面就会有问题,因为静态类型的变量,属于这个类的所有对象
  • sp3应该有自己的一个_count,然而这时的sp3中的_count被构造函数重新构造了
  • 静态的_count被改成了1,导致析构的时候,sp3的_count变成了0,sp2将_count变成了-1,sp1将_count变成了-2

导致sp2 和 sp1的共同管理的空间没有被释放掉。

应该释放两次,我们期望一个资源跟一个计数,并且我改变,你也要跟着变。

核心点:

  • 可以几个智能指针共同管理同一个资源,这几个智能指针的计数可以相互影响
  • 但是当另一块资源也需要被管理时,就需要另一个智能指针来管理
  • 这个智能指针里的计数和上述的智能指针里的计数互不影响

模拟实现:

namespace YY
{//指针版本(可采用)//要想拷贝就用shared_ptrtemplate<class T>class shared_ptr{public:void Release(){if (--(*_pCount) == 0 && _ptr){cout << "delete:" << _ptr << endl;//将资源释放掉delete _ptr;_ptr = nullptr;//将计数释放掉,因为也是new出来的,在堆上delete _pCount;_pCount = nullptr;}}//RAII思想shared_ptr(T* ptr):_ptr(ptr), _pCount(new int(1)){}~shared_ptr(){Release();}//不仅拷贝资源的指针,还拷贝计数的指针 -- 此时用的就是同一个计数shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pCount(sp._pCount){(*_pCount)++;}//sp1 = sp3shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (this != &sp)//	if (_ptr != sp._ptr){//--(*_pCount);//减减之后还要释放,不然内存泄露//sp1和sp2都交给sp3管理了Release();_ptr = sp._ptr;_pCount = sp._pCount;++(*_pCount);}return *this;}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}private:T* _ptr;//搞一个计数,直接定义一个变量不行,因为一个对象中_count的改变不会影响另一个对象的int* _pCount;};
}

拷贝构造:

  • 说明又增加了一个指向该空间管理的指针
  • 引用计数++

在这里插入图片描述
赋值重载:

  • 现将原来的指向的空间释放掉或者计数 - -
  • 再将指针赋值过来,将新指向这块空间的指针计数++
    在这里插入图片描述

可以正常的析构掉:
在这里插入图片描述
相同的资源实现了用两个指针来管理,同时实现了管理相同资源用同一个计数。

  • 我们通过传指针的方式,拷贝构造时直接将计数的指针赋值
  • 这样就实现了拷贝之后用同一个计数

这里还存在一些多线程的问题,我们以后再说~~


3. 循环引用

我们先来看一段代码:

struct ListNode
{ListNode* _next = nullptr;ListNode* _prev = nullptr;~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{ListNode* p1 = new ListNode;ListNode* p2 = new ListNode;p1->_next = p2;p2->_prev = p1;//中间抛异常了还释放不了,考虑用智能指针delete p1;delete p2;return 0;
}

能够正常释放:
在这里插入图片描述

我们简单的实现了以下双向链表的部分:

  • 在申请结点和释放结点之间如果抛了异常
  • 就会存在内存泄漏的问题
  • 所以此时就有小伙伴想用智能指针来管理结点

问题接踵而至~

  • 那就是链接的时候匹配不上,类内部用原生指针
  • 外面链接的时候则是用智能指针链接
  • 那就只能将类内部的原生指针换成智能指针

这不换不要紧,一换就出了大问题~~😱😱😱

struct ListNode
{YY::shared_ptr<ListNode> _prev = nullptr;YY::shared_ptr<ListNode> _next = nullptr;~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{YY::shared_ptr<ListNode> p1(new ListNode);YY::shared_ptr<ListNode> p2(new ListNode);p1->_next = p2;p2->_prev = p1;return 0;
}

运行结果啥都没有,说明内存泄露了,哪个结点都没释放!!

3.1 循环引用的过程:

为了更好的理解,看下图:

在这里插入图片描述
从图中我们可以清晰看出来:

  • p1和右边结点里的_prve共同管理着左边结点
  • p2和左边结点里的_next共同管理着右边结点
  • 当一个结点中有自定义类型的时候,其自定义类型的对象析构是在该结点析构的时候才析构的。
  • 一个对象析构时,该对象内置类型不处理,自定义类型调用其析构函数。

重点:

  • 右边结点析构,右边结点的_prve才能析构
  • 左边结点的_next析构,右边结点才能析构
  • 左边结点析构,左边结点的_next才能析构
  • 右边结点的_prve析构,左边结点才能析构
  • 右边结点析构,右边结点的_prve才能析构
  • ……

直接无限循环了,一整个尬住了。。。😅😅😅

只有其中一个智能指针不引用计数,才能打破这个循环~


3.2 weak_ptr:

在这里插入图片描述

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

在这里插入图片描述

  • 可以说weak_ptrshared_ptr的小弟 — 不是传统的智能指针
  • 专门用来辅助解决shared_ptr循环引用的问题

模拟实现:

namespace YY
{//不参与指向资源的释放管理,意义就是解决循环引用的问题//不需要RAIItemplate<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){if (_ptr != sp.get()){_ptr = sp.get();}return *this;}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}public:T* _ptr;};
}

在这里插入图片描述
在这里插入图片描述