从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

目录
相关文章
|
19天前
|
C++ 容器
【编程技巧】 C++11智能指针
C++11引入了智能指针以自动管理内存,防止内存泄漏和悬挂指针: - `shared_ptr`:引用计数,多所有权,适用于多个对象共享资源。 - `unique_ptr`:独占所有权,更轻量级,适用于单一对象所有者。 - `weak_ptr`:弱引用,不增加引用计数,解决`shared_ptr`循环引用问题。 ## shared_ptr - 支持引用计数,所有者共同负责资源释放。 - 创建方式:空指针、new操作、拷贝构造/移动构造,以及自定义删除器。 - 提供`operator*`和`operator-&gt;`,以及`reset`、`swap`等方法。 ## unique_ptr
228 2
|
24天前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。
|
22天前
|
设计模式 C++ 开发者
C++一分钟之-智能指针:unique_ptr与shared_ptr
【6月更文挑战第24天】C++智能指针`unique_ptr`和`shared_ptr`管理内存,防止泄漏。`unique_ptr`独占资源,离开作用域自动释放;`shared_ptr`通过引用计数共享所有权,最后一个副本销毁时释放资源。常见问题包括`unique_ptr`复制、`shared_ptr`循环引用和裸指针转换。避免这些问题需使用移动语义、`weak_ptr`和明智转换裸指针。示例展示了如何使用它们管理资源。正确使用能提升代码安全性和效率。
22 2
|
22天前
|
C语言 图形学 C++
|
4天前
|
安全 编译器 程序员
【C++11】智能指针
【C++11】智能指针
4 0
|
1月前
|
编译器 Linux C++
C++智能指针
**C++智能指针是RAII技术的体现,用于自动管理动态内存,防止内存泄漏。主要有三种类型:已废弃的std::auto_ptr、不可复制的std::unique_ptr和可共享的std::shared_ptr。std::unique_ptr通过禁止拷贝和赋值确保唯一所有权,而std::shared_ptr使用引用计数来协调多个指针对同一资源的共享。在C++17中,std::auto_ptr因设计缺陷被移除。**
|
1月前
|
C语言 C++ 编译器
【C++语言】冲突-C语言:输入输出、缺省参数、引用、内联函数
【C++语言】冲突-C语言:输入输出、缺省参数、引用、内联函数
【C++语言】冲突-C语言:输入输出、缺省参数、引用、内联函数
|
19天前
|
自然语言处理 C语言 C++
程序与技术分享:C++写一个简单的解析器(分析C语言)
程序与技术分享:C++写一个简单的解析器(分析C语言)
|
22天前
|
程序员 C语言 C++
【C语言】:柔性数组和C/C++中程序内存区域划分
【C语言】:柔性数组和C/C++中程序内存区域划分
14 0
|
1月前
|
C语言 C++
【C++语言】冲突-C语言:命名空间
【C++语言】冲突-C语言:命名空间