【C++】一文深入浅出带你参透库中的几种 [ 智能指针 ]及其背后实现原理(代码&图示)

简介: 【C++】一文深入浅出带你参透库中的几种 [ 智能指针 ]及其背后实现原理(代码&图示)

前言

大家好吖,欢迎来到 YY 滴C++系列 ,热烈欢迎! 本章主要内容面向接触过C++的老铁

主要内容含:

一.前言:智能指针出现解决内存泄漏问题

通俗语言介绍:

  • 抛异常捕获异常的过程中 ,有可能会影响 执行流
  • try+catch 程序的流程是:运行到try块中,如果有异常抛出,则转到catch块去处理。然后执行catch块 后面 的语句
int main()
{
  try
  {
    pair<string, string>* p1 = new pair<string, string>;

    f();//如果在f函数中抛出异常,会直接跳到catch块后面的语句,导致p1资源没有被释放,造成内存泄漏
    delete p1;
  }
  catch (const exception& e)
  {
    cout << e.what() << endl;
  }
  //捕获异常后跳转到的位置

  return 0;
}

二.RAII

通俗语言介绍:

  • 设置一个类 ,可以构造和析构,交给这个类的对象来 管理 指针。保证其一定会释放资源,不会内存泄漏。


简介:

  • RAII(Resource Acquisition Is Initialization):即 资源获取时即初始化
  • RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
    存、文件句柄、网络连接、互斥量等等)的简单技术。
  • 在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
  • 借此,我们实际上把管理一份资源的 责任 托管给了一个对象。

RALL的两大好处:

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

三.实现一个简单的智能指针

  • 一个智能指针需要具备以下两种功能
  1. RAII管控资源释放 (资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源),即有构造有析构
  2. 像指针一样使用 ,即有 解引用功能 和 “->”功能
  • 为了能够匹配更多类型,我们可以采用类模板的方式处理
template<class T>
class SmartPtr
{
public:
  // RAII
  // 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源
  // 1、RAII管控资源释放
  // 2、像指针一样
  
//RAII
  SmartPtr(T* ptr)
    :_ptr(ptr)
  {}
  ~SmartPtr()
  {
    cout << "delete:" << _ptr << endl;
    delete _ptr;
  }
//像指针一样,重载*和->
  T& operator*()
  {
    return *_ptr;
  }
  T* operator->()
  {
    return _ptr;
  }
private:
  T* _ptr;
};

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

  return a / b;
}

void f()
{
  // 21:15继续
  SmartPtr<pair<string, string>> sp1(new pair<string, string>("1111", "22222"));
  //div();
  SmartPtr<pair<string, string>> sp2(new pair<string, string>);
  SmartPtr<pair<string, string>> sp3(new pair<string, string>);
  SmartPtr<string> sp4(new string("xxxxx"));

//像指针一样
  cout << *sp4 << endl;
  cout << sp1->first << endl;
  cout << sp1->second << endl;

  div();
  
//有了智能指针来管理,不需要我们手动释放资源
  //delete p1;
  //cout << "delete:" << p1 << endl;
}

int main()
{
  try
  {
    f();
  }
  catch (const exception& e)
  {
    cout << e.what() << endl;
  }

  return 0;
}

四.简单的智能指针会遇到的问题:(浅)拷贝问题

  • 当我们用三设计的智能指针进行拷贝时,我们会发现程序会出现一个问题
  • 因为我们没有写默认的拷贝,所以类会生成一个默认的拷贝(浅)拷贝
  • 浅拷贝只是把指针指向那块空间,因此析构时便会对同一块空间析构两次,且原来开的空间没有释放造成内存泄漏

函数运行结果如下所示:可以发现其析构了两次

int main()
{
  SmartPtr<string> sp1(new string("xxxxx"));
  SmartPtr<string> sp2(new string("yyyyy"));

  sp1 =sp2;

  return 0;
}
  • 让我们接下来看看boost库和后来的C++11是如何解决这个问题的

五.库中的几种智能指针

※几种智能指针简述

1.智能指针演变史

  • 最初的智能指针是C++98中的auto_ptr,但它有很明显的缺陷(下文会介绍)
  • 主流智能指针(unique,shared,weak)一开始是由boost库中创建,后面被C++11沿用
  • scpoe-ptr换成了auto_ptr,两者本质差不多


2.智能指针简述

  1. auto_ptr:(管理权直接转移,导致被拷贝对象悬空,访问就会出问题)(auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr)
  1. unique_ptr:(很粗暴,直接不允许拷贝,不需要拷贝的场景建议使用)
  2. share_ptr:(引用计数支持拷贝,需要拷贝的场景使用;要小心构成【循环引用】导致内存泄漏)
  3. weak_ptr:(专门解决share_ptr【循环引用】问题)

1)auto_ptr

class A
{
public:
  A(int a = 0)
    :_a(a)
  {
    cout << "A(int a = 0)" << endl;
  }

  ~A()
  {
    cout << this;
    cout << " ~A()" << endl;
  }
//private:

  int _a;
};


int main()
{
  // C++98 一般实践中,很多公司明确规定不要用这个
  auto_ptr<A> ap1(new A(1));
  auto_ptr<A> ap2(new A(2));

  // 管理权转移,拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象
  // 隐患:导致被拷贝对象悬空,访问就会出问题
  auto_ptr<A> ap3(ap1);

  // 崩溃
  //ap1->_a++;
  ap3->_a++;

  return 0;
}

2)unique_ptr

  • unique_ptr的实现原理:简单粗暴的防拷贝
  • 顾名思义:unique-唯一的
class A
{
public:
  A(int a = 0)
    :_a(a)
  {
    cout << "A(int a = 0)" << endl;
  }

  ~A()
  {
    cout << this;
    cout << " ~A()" << endl;
  }
//private:

  int _a;
};

int main()
{
  // C++11  简单粗暴,不让拷贝
  unique_ptr<A> up1(new A(1));
  unique_ptr<A> up2(new A(2));

  unique_ptr<A> up3(up1);//无法拷贝
  
  return 0;
}

3)shared_ptr

引入:

  • 我们如果在拷贝时直接让他指向资源,那么当程序结束时就会进行两次析构; 如果我们设置一个计数 控制 这个析构过程;问题就解决了
  • C++11中开始提供更靠谱的并且 支持拷贝 的shared_ptr
  • shared_ptr的原理:是通过 引用计数 的方式来实现多个shared_ptr对象之间 共享 资源
  1. shared_ptr在其内部, 给每个资源都维护了着一份计数 ,用来记录该份资源被几个对象共享;
  2. 在对象被销毁时(也就是析构函数调用),,就说明自己不使用该资源了,对象的引用计数减一;
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了;
  • 一个基本shared_ptr的实现如下所示
template<class T>
  class shared_ptr
  {
  public:
    // RAII
    // 像指针一样
    shared_ptr(T* ptr = nullptr)
      :_ptr(ptr)
      ,_pcount(new int(1))//动态开辟,不可以静态,因为有可能有多个智能指针类管理不同的资源
    {}

    ~shared_ptr()
    {
      if (--(*_pcount) == 0)       //析构前要判断引用计数
      {
        cout << "delete:" << _ptr << endl;
        delete _ptr;
        delete _pcount;
      }
    }

    T& operator*()
    {
      return *_ptr;
    }

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

    // sp3(sp1)
    shared_ptr(const shared_ptr<T>& sp)      //拷贝时,让新的对象的指针成为这个智能指针类管理的指针
      :_ptr(sp._ptr)
      ,_pcount(sp._pcount)
    {
      ++(*_pcount);
    }

//解决自己拷贝自己时,引用计数会增加的问题——————>加入一个判断
// sp1 = sp1
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
      if (_ptr == sp._ptr)
        return *this;

      if (--(*_pcount) == 0)
      {
        delete _ptr;
        delete _pcount;
      }

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

      return *this;
    }
    
//涉及到后面与weak_ptr的搭配问题,不让他直接指向资源(防止引用计数增加)
    int use_count() const
    {
      return *_pcount;
    }
    T* get() const
    {
      return _ptr;
    }

  private:
    T* _ptr;
    int* _pcount;      //动态的引用计数
  };
class A
{
public:
  A(int a = 0)
    :_a(a)
  {
    cout << "A(int a = 0)" << endl;
  }

  ~A()
  {
    cout << this;
    cout << " ~A()" << endl;
  }
//private:

  int _a;
};

int main()
{
  // C++11
    shared_ptr<A> sp1(new A(1));

  shared_ptr<A> sp3(sp1);//拷贝以后
  
  sp1->_a++;
  sp3->_a++;//程序不会崩溃

  return 0;
}
  • 引用计数的实现如下图所示:

六.解决shared_ptr的循环引用问题而生:weak_ptr

1)了解【循环引用】问题

  • 我们在使用share_ptr时,有时会遇到以下这种场景
  • 例如:双向链接两个节点
template<class T>
  class shared_ptr
  {
  public:
    // RAII
    // 像指针一样
    shared_ptr(T* ptr = nullptr)
      :_ptr(ptr)
      ,_pcount(new int(1))//动态开辟,不可以静态,因为有可能有多个智能指针类管理不同的资源
    {}

    ~shared_ptr()
    {
      if (--(*_pcount) == 0)       //析构前要判断引用计数
      {
        cout << "delete:" << _ptr << endl;
        delete _ptr;
        delete _pcount;
      }
    }

    T& operator*()
    {
      return *_ptr;
    }

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

    // sp3(sp1)
    shared_ptr(const shared_ptr<T>& sp)      //拷贝时,让新的对象的指针成为这个智能指针类管理的指针
      :_ptr(sp._ptr)
      ,_pcount(sp._pcount)
    {
      ++(*_pcount);
    }

//解决自己拷贝自己时,引用计数会增加的问题——————>加入一个判断
// sp1 = sp1
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
      if (_ptr == sp._ptr)
        return *this;

      if (--(*_pcount) == 0)
      {
        delete _ptr;
        delete _pcount;
      }

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

      return *this;
    }

    int use_count() const
    {
      return *_pcount;
    }

//涉及到后面与weak_ptr的搭配问题,不让他直接指向资源(防止引用计数增加)
    T* get() const
    {
      return _ptr;
    }

  private:
    T* _ptr;
    int* _pcount;      //动态的引用计数
  };
  
struct Node
{
  A _val;
  //不能如此使用,因为后面要让_next&_prev指向的都是智能指针的对象,sp1->_next = sp2;会出现类型不匹配的问题
  //Node* _next;
  //Node* _prev;
 
 //要把节点指针也用智能指针包装一下才可以
   shared_ptr<Node> _next;
   shared_ptr<Node> _prev;

};

int main()
{
  // 循环引用
  shared_ptr<Node> sp1(new Node);
  shared_ptr<Node> sp2(new Node);

  cout << sp1.use_count() << endl;//1
  cout << sp2.use_count() << endl;//1

  sp1->_next = sp2;
  sp2->_prev = sp1;

  cout << sp1.use_count() << endl;//1
  cout << sp2.use_count() << endl;//1
//进入死循环
  return 0;
}

  • 我们要注意到,因为为了能sp1->_next能够实现,我们让Node的_next&_prev指针都由智能指针托管
  • 所以当 sp1->_next或者sp1->_prev时,本质上是一种拷贝,会导致资源的引用计数增加

程序执行以后,我们会发现析构时,引用计数仍然为1,变成死循环了,具体过程如下图所示


2)利用weak_ptr 解决【循环引用】问题

  • 在shared_ptr中封装一层函数use_count()get()
  • 设置一个weak_ptr,在拷贝构造&赋值运算符重载环节引入上一步骤中封装的函数
  • 原理:
  1. 在设置Node节点的指针时,用weak_ptr来处理, 使得它和share_ptr是同一类型
  2. weak_ptr 不增加引用计数 ,可以访问资源,不参与资源释放的管理
template<class T>
  class shared_ptr
  {
//...略去的内容
    int use_count() const
    {
      return *_pcount;
    }
    T* get() const
    {
      return _ptr;
    }

  private:
    T* _ptr;
    int* _pcount;      //动态的引用计数
  };
  
struct Node
{
  A _val;
  //shared_ptr<Node> _next;
  //shared_ptr<Node> _prev;

    weak_ptr<Node> _next;
    weak_ptr<Node> _prev;
  // weak_ptr不参与资源释放的管理的RAII智能指针,专门用来解决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;
  };

int main()
{
  // 循环引用
  bit::shared_ptr<Node> sp1(new Node);
  bit::shared_ptr<Node> sp2(new Node);

  cout << sp1.use_count() << endl;
  cout << sp2.use_count() << endl;

  sp1->_next = sp2;
  sp2->_prev = sp1;

  cout << sp1.use_count() << endl;
  cout << sp2.use_count() << endl;

  return 0;
}

七.shared_ptr的【定制删除器】:管理不同方式产生的对象

引入:

  • 我们在查看智能指针的文档时,会发现图中这种形式,这就是库里的定制删除器
  • 如果不是new出来的对象如何通过智能指针管理呢?定制删除器就是用来解决这个问题
  • 我们 只要往后面传一个可调用对象(删除方法)即可 : 仿函数(函数对象),lambda,函数指针都可以作为参数
template<class T>
struct DeleteArray
{
  void operator()(T* ptr)
  {
    delete[] ptr;
  } 
};

int main()
{
  //仿函数
  shared_ptr<A> sp1(new A[10], DeleteArray<A>());
  
  //lambda表达式
  shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });

    shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {
    fclose(ptr);
    });
  
  //默认是delete
  shared_ptr<A> sp4(new A);

  return 0;
}

相关文章
|
4天前
|
存储 程序员 C++
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
22 0
|
1月前
|
算法 安全 C++
提高C/C++代码的可读性
提高C/C++代码的可读性
52 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
86 4
|
2月前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
2月前
|
存储 搜索推荐 C语言
深入C语言指针,使代码更加灵活(二)
深入C语言指针,使代码更加灵活(二)
|
2月前
|
存储 程序员 编译器
深入C语言指针,使代码更加灵活(一)
深入C语言指针,使代码更加灵活(一)
|
2月前
|
C语言
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
|
2月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
61 1
|
2月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
43 2
|
2月前
|
Linux C语言 C++
vsCode远程执行c和c++代码并操控linux服务器完整教程
这篇文章提供了一个完整的教程,介绍如何在Visual Studio Code中配置和使用插件来远程执行C和C++代码,并操控Linux服务器,包括安装VSCode、安装插件、配置插件、配置编译工具、升级glibc和编写代码进行调试的步骤。
355 0
vsCode远程执行c和c++代码并操控linux服务器完整教程
下一篇
DataWorks