从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

目录
相关文章
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
419 1
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
590 7
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
存储 人工智能 Java
一文轻松拿捏C语言的指针的基础使用
本文介绍了C语言中的指针概念,包括直接访问和间接访问内存的方式、指针变量的定义与使用、取址运算符`&`和取值运算符`*`的应用,帮助读者深入理解指针这一C语言的核心概念。君志所向,一往无前!
401 0
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
2093 13
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
1837 9
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
688 11
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
615 7
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
990 3