从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++ 11新特性之unique_ptr
C++ 11新特性之unique_ptr
413 4
|
安全 C++ 开发者
C++ 11新特性之shared_ptr
C++ 11新特性之shared_ptr
297 0
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
安全 NoSQL Redis
C++新特性-智能指针
C++新特性-智能指针
|
安全 编译器 容器
C++STL容器和智能指针
C++STL容器和智能指针
|
存储 Java 程序员
面向对象编程(C++篇4)——RAII
面向对象编程(C++篇4)——RAII
132 0
|
10月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
6月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
185 0
|
6月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
279 0
|
8月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
319 12

热门文章

最新文章