从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr(中)

简介: 从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr

从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr(上):https://developer.aliyun.com/article/1522495

3.1 auto_ptr模拟代码

(上面SmartPtr再加一个赋值重载改下名字就差不多是auto_ptr的模拟了,再用命名空间封一下)

赋值重载细节还挺多的,前面学的赋值重载都类似拷贝构造,可以不看先写写,这里直接放代码:

#include <iostream>
#include <memory>
using namespace std;
//1、RAII
//2、像指针一样
//3、解决拷贝问题(不同的智能指针的解决方式不一样)
 
namespace rtx
{
  template<class T>
  class auto_ptr
  {
  public:
    auto_ptr(T* ptr)
      :_ptr(ptr)
    {}
    ~auto_ptr()
    {
      cout << "~auto_ptr -> delete: " << _ptr << endl;
      delete _ptr;
    }
    auto_ptr(auto_ptr<T>& ptr)
      :_ptr(ptr._ptr)
    {
      ptr._ptr = nullptr;
    }
    auto_ptr<T>& operator=(auto_ptr<T>& ap)
    {
      if (this != &ap) // 防止自己赋值给自己
      {
        if (_ptr) // 防止释放空,delete空也行
        {
          cout << "operator= -> Delete:" << _ptr << endl;
          delete _ptr;
        }
        _ptr = ap._ptr;
        ap._ptr = nullptr;
      }
      return *this;
    }
 
    T& operator*()
    {
      return *_ptr;
    }
 
    T* operator->()
    {
      return _ptr;
    }
  protected:
    T* _ptr;
  };
}
 
class A
{
public:
  ~A()
  {
    cout << "~A()" << endl;
  }
//protected:
  int _a1 = 0;
  int _a2 = 0;
};
 
int main()
{
  //SmartPtr<A> sp1(new A);
  //SmartPtr<A> sp2(sp1);
  rtx::auto_ptr<A> sp1(new A);
  rtx::auto_ptr<A> sp2(sp1);
  rtx::auto_ptr<A> sp3 = sp2;
 
  return 0;
}

eebe7680ce4e450e939f340c8319f427.png        可以把命名空间切换到std比较一下,auto_ptr使用的是管理权转移的办法,会导致被拷贝对象悬空,是不负责的拷贝,对于不清楚auto_ptr这个特点的人来说,拷贝后再次使用ap1就会出问题。auto_ptr是C++98一个失败的设计,被挂在了耻辱柱上,很多公司明确要求不能使用auto_ptr。


       C++98至C++11期间人们被迫用C++更新探索的库:boost库里的一些智能指针,到了C++11,终于更新了三个智能指针:unique_prt,shared_ptr,wead_ptr,相当于抄boost库的作业了。下面我们介绍以及模拟实现这几个智能指针,当然,还有很多接口在模拟代码里没有实现。

4. unique_ptr

在C++11中更加靠谱的unique_ptr智能指针:

unique_ptr直接禁止使用拷贝构造函数,即使编译器也不能生成默认的拷贝构造函数,因为使用了delete关键字。

       unique_ptr采用的策略就是,既然拷贝有问题,那么就直接禁止拷贝,这确实解决了悬空等问题,使得unique_ptr是一个独一无二的智能指针。

(写到这发现忘记创建新项目了,这里创建一个Test.cpp和SmartPtr.hpp(.h+.cpp,直接.h也行,都可以把函数的实现在里面实现。声明和定义分离只是为了保护源码)


4.1 unique_ptr模拟代码

直接复制一份auto_ptr代码过来,用delete关键字禁言拷贝构造和赋值重载就行了:

  template<class T>
  class unique_ptr
  {
  public:
    unique_ptr(T* ptr)
      :_ptr(ptr)
    {}
    ~unique_ptr()
    {
      cout << "~unique_ptr -> delete: " << _ptr << endl;
      delete _ptr;
    }
    unique_ptr(unique_ptr<T>& ptr) = delete;
    unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
 
    T& operator*()
    {
      return *_ptr;
    }
    T* operator->()
    {
      return _ptr;
    }
  protected:
    T* _ptr;
  };

25dc9aa252e14b9787aeb027488d5c8f.png

关于delete关键字的复习链接:(在5.2)从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值_GR_C的博客-CSDN博客

5. shared_ptr

unique_ptr禁掉了拷贝,但是如果我就想拷贝智能指针呢?这就要用到shared_ptr了:

        shared_ptr采用了引用计数的方法来解决拷贝问题:(引用计数直接在成员变量加一个int Count可以吗?每一个对象都有一个自己的Count显然是不对的,我们应该让拷贝和被拷贝对象管理同一个Count。那么使用静态成员变量可以吗?这也不可以,因为这样所有的对象都管理的是同一个Count了,包括没有拷贝的对象)


shared_ptr原理:


通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

       例如:老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。

① shared_ptr内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享。

② 在对象被销毁时(也就是析构函数调用),说明自己不使用该资源了,对象引用计数减一。

③ 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。

④ 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

shared_ptr增加了一个成员类似int* _pCount解决这个问题:

这样构造,拷贝构造和析构函数就是这样的:

       构造先给 _pCount指向1,析构无论什么时候都减减,如果减减0就释放资源,拷贝构造就是把指针也给它,然后指针指向的内容加加。

到这可以自己尝试写一个赋值重载出来,手写或者敲都行OK,这里直接放代码了:

5.1 shared_ptr模拟代码

  template<class T>
  class shared_ptr
  {
  public:
    shared_ptr(T* ptr = nullptr)
      : _ptr(ptr)
      , _pCount(new int(1))
    {}
 
    void Release()
    {
      if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数
      {
        delete _ptr;
        delete _pCount;
      }
    }
    ~shared_ptr()
    {
      Release();
    }
 
    shared_ptr(const shared_ptr<T>& sp)
      : _ptr(sp._ptr)
      , _pCount(sp._pCount)
    {
      (*_pCount)++;
    }
 
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
      //if (this != &sp)
      if (_ptr != sp._ptr) // 防止自己给自己赋值,注意不能比较this,类似s1 = s2; 再来一次s1 = s2;
      {                    // 比较_pCount也行
        //if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数
        //{
        //  delete _ptr;
        //  delete _pCount;
        //}
        Release();
 
        _ptr = sp._ptr;
        _pCount = sp._pCount;
        (*_pCount)++;
      }
      return *this;
    }
 
    T& operator*()
    {
      return *_ptr;
    }
    T* operator->()
    {
      return _ptr;
    }
  protected:
    T* _ptr;
    int* _pCount;// 引用计数,有多线程安全问题,学了linux再讲,不能用静态成员
  };

赋值重载需要注意细节的都在注释写了,可以自己画一个图看看。


5.2 循环引用

       shared_ptr 完美了吗?并不是,它有一个死穴:循环引用。创建一个链表节点,在该节点的析构函数中打印提示信息:

struct Node
{
  ~Node()
  {
    cout << "~Node" << endl;
  }
 
  int _val;
  std::shared_ptr<Node> _next;
  std::shared_ptr<Node> _prev;
};

将n1和n2互相指向,形成循环引用:

(因为要给_next和_prev赋值,所以Node里也要用智能指针)

int main()
{
  std::shared_ptr<Node> n1(new Node);
  std::shared_ptr<Node> n2(new Node);
 
  n1->_next = n2;
  n2->_prev = n1;
 
  return 0;
}

执行该程序后,节点析构函数中的打印信息并没有打印,说明析构出了问题。

如果不形成循环引用就会打印提示信息:

可以调用shared_ptr里的use_count接口打印引用计数值:

       n1和n2刚创建的时候,它两的引用计数值都是1。当两个节点循环引用后,它们的引用计数值都变成了2。

n2先析构,右边的引用计数变为1,n1再析构,左边的引用计数变为1,然后就没了。


左边结点的_next什么时候释放?-> 取决于左边的结点什么时候delete。


左边的结点什么时候delete?-> 取决于右边结点的_prev。


右边结点的_prev什么时候释放?-> 取决于右边的结点什么时候delete。


右边的结点什么时候delete?-> 取决于左边结点的_next。


左边结点的_next什么时候释放? -> 回到一开始的问题,进入死循环。


在循环引用中,节点得不到真正的释放,就会造成内存泄漏。


循环引用的根本原因在于,next和prev也参与了资源的管理。


       这个漏洞shared_ptr本身也解决不了,所以就增加了weak_ptr来解决这个问题。解决办法就是让节点中的_next和_prev仅指向对方,而不参与资源管理,也就是计数值不增加。


这里为了配合上面和给下面模拟weak_ptr演示给我们的shared_ptr加两个接口函数:

c460056e6bd14fec94a2c8d0d2c8313d.png

从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr(下):https://developer.aliyun.com/article/1522498?spm=a2c6h.13148508.setting.23.50c04f0ef94tTt

目录
相关文章
|
7天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
25 4
|
23天前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
1月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
38 1
|
1月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
29 2
|
1月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
33 3
|
6天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
21 6
|
26天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
34 10
|
19天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
25天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
53 7