【C++】智能指针(下)

简介: 【C++】智能指针(下)

std::shared_ptr


shared_ptr 是更靠谱的并且支持拷贝的智能指针。shared_ptr 的原理:是通过引用计数的方式来实现多个shared_ptr 对象之间共享资源。


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

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

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

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


    share_ptr 的演示使用


    a60b2fd0f8b14bd2aef5e0d5e6a28254.png


    注:以下的实现方式是错误的。


    namespace Joy
    {
      template<class T>
      class shared_ptr
      {
      public:
        shared_ptr(T* ptr = nullptr)
          : _ptr(ptr)
        {
          ++_count;
        }
        ~shared_ptr()
        {
          if (--_count == 0 && _ptr)
          {
            cout << "Delete:" << _ptr << endl;
            delete _ptr;
          }
        }
        shared_ptr(shared_ptr<T>& sp)
          : _ptr(sp._ptr)
        {
          ++_count;
        }
        T& operator*()
        {
          return *_ptr;
        }
        T* operator->()
        {
          return _ptr;
        }
        int* GetCount() const
        {
          return &_count;
        }
      private:
        T* _ptr;
        static int _count; // 引用计数
      };
      template<class T>
      int shared_ptr<T>::_count = 0;
      void SharedPtrTest1()
      {
        Joy::shared_ptr<A> sp1(new A);
        Joy::shared_ptr<A> sp2(sp1);
        Joy::shared_ptr<A> sp3(sp2);
        Joy::shared_ptr<int> sp4(new int);
        Joy::shared_ptr<A> sp5(new A);
        Joy::shared_ptr<A> sp6(sp5);
      }
    }
    

    e61a771876b64b4d94fb59c842ad914a.png

    f43ca95839714e93b72d147b153a8839.png

    shared_ptr 的模拟实现


    namespace Joy
    {
      template<class T>
      class shared_ptr
      {
      public:
        shared_ptr(T* ptr = nullptr)
          : _ptr(ptr)
          , _pCount(new int(1))
        {}
        shared_ptr(const shared_ptr<T>& sp)
          : _ptr(sp._ptr)
          , _pCount(sp._pCount)
        {
          ++(*_pCount);
        }
        void Release()
        {
          // 减减对象的计数,如果是最后一个对象,要释放资源
          if (--(*_pCount) == 0)
          {
            cout << "Delete:" << _ptr << endl;
            delete _ptr;
            delete _pCount;
          }
        }
        ~shared_ptr()
        {
          Release();
        }
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
          // 通过_ptr来判断管理的资源是否是同一个。如果是同一个,
          // _ptr相等,直接返回即可
          //if(this == &sp)
          if (_ptr == sp._ptr)
          {
            return *this;
          }
          Release();  // 减减被赋值对象的计数,如果是最后一个对象,要释放资源
          // 共管新资源,++计数
          _ptr = sp._ptr;
          _pCount = sp._pCount;
          ++(*_pCount);
          return *this;
        }
        T& operator*()
        {
          return *_ptr;
        }
        T* operator->()
        {
          return _ptr;
        }
        int* GetCount() const
        {
          return _pCount;
        }
      private:
        T* _ptr;
        int* _pCount; // 引用计数
      };
      void SharedPtrTest2()
      {
        Joy::shared_ptr<A> sp1(new A);
        Joy::shared_ptr<A> sp2(sp1);
        Joy::shared_ptr<A> sp3(sp2);
        cout << sp1.GetCount() << endl;
        cout << sp2.GetCount() << endl;
        cout << sp3.GetCount() << endl;
        cout << "--------" << endl;
        Joy::shared_ptr<int> sp4(new int);
        cout << sp4.GetCount() << endl;
        cout << "--------" << endl;
        Joy::shared_ptr<A> sp5(new A);
        Joy::shared_ptr<A> sp6(sp5);    
        cout << sp5.GetCount() << endl;
        cout << sp6.GetCount() << endl;
        cout << "--------" << endl;
      }
    }
    

    51f2687f7aad4998a4b081debcd9262a.png


    注:shared_ptr 的赋值运算符重载需要防止自己给自己赋值的情况。只要两个对象管理的资源是一样的,那么这两个对象就不要相互赋值,直接 return 即可。我们可以通过 _ptr 或者 _pCount 是否相等来判断是否管理同一个资源。


    std::shared_ptr的循环引用


    struct Node
    {
      int _val;
      // 注:Node*和std::shared_ptr<Node>不是同一个类型
      std::shared_ptr<Node> _prev;
      std::shared_ptr<Node> _next;
      ~Node()
      {
        cout << "~Node()" << endl;
      }
    };
    // shared_ptr的循环引用
    void SharedPtrTest3()
    {
      std::shared_ptr<Node> node1(new Node);
      std::shared_ptr<Node> node2(new Node);
      node1->_next = node2;
      node2->_prev = node1;
    }
    

    28dd9ffd05434154b95711541ac1dc6f.png


    循环引用分析


    node1 和 node2 两个智能指针对象指向两个节点,引用计数变成 1,我们不需要手动 delete。

    node1 的 _next 指向 node2,node2 的_prev 指向node1,引用计数变成 2。

    node1 和 node2 析构,引用计数减到 1,但是 _next 还指向下一个节点,_prev 还指向上一个节点。

    也就是说 _next 析构了,node2 才能释放。

    也就是说 _prev 析构了,node1 才能释放。

    但是 _next 属于 node1 的成员,node1 释放了,_next 才会析构。而 node1 由 _prev 管理,_prev 属于node2 成员,node2 释放了,_prev 才会行。所以这就叫循环引用,谁也不会释放。

    aee7a4ea17eb496ab5f18af48067ed6c.png


    55ab880bcacf4877a18108a2f844b7c0.png


    shared_ptr 的循环引用问题通过 weak_ptr 来解决。weak_ptr 并不是常规的智能指针,没有 RAII,不支持直接管理资源。weak_ptr 主要用 shared_ptr 来构造,用来解决 shared_ptr 的循环引用问题。

    528344ee77c5414d9b31ce245a4fd144.png


    weak_ptr 调用拷贝构造和赋值运算符重载是不会增加资源的引用计数的,它不参与资源的管理。


    struct Node
    {
      int _val;
      std::weak_ptr<Node> _prev;
      std::weak_ptr<Node> _next;
      ~Node()
      {
        cout << "~Node()" << endl;
      }
    };
    // shared_ptr的循环引用
    void SharedPtrTest4()
    {
      std::shared_ptr<Node> node1(new Node);
      std::shared_ptr<Node> node2(new Node);
      // 注:use_count函数返回的是引用计数的大小
      cout << node1.use_count() << endl;
      cout << node2.use_count() << endl;
      cout << "--------" << endl;
      node1->_next = node2;
      node2->_prev = node1;
      cout << node1.use_count() << endl;
      cout << node2.use_count() << endl;
      cout << "--------" << endl;
    }
    

    46912ecb2ff34cde9e579a06de96784c.png


    weak_ptr 的简单模拟实现


    namespace Joy
    {
      // weak_ptr是辅助型智能指针,主要用于解决shared_ptr的循环引用问题
      template <class T>
      class weak_ptr
      {
      public:
        weak_ptr()
          : _ptr(nullptr)
        {}
        weak_ptr(const shared_ptr<T>& sp)
          : _ptr(sp.get())
        {}
        weak_ptr(const weak_ptr<T>& wp)
          : _ptr(wp._ptr)
        {}
        weak_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
          _ptr = sp.get();
          return *this;
        }
        weak_ptr<T>& operator=(const weak_ptr<T>& wp)
        {
          _ptr = wp._ptr;
          return *this;
        }
        T& operator*()
        {
          return *_ptr;
        }
        T* operator->()
        {
          return _ptr;
        }
      private:
        T* _ptr;
      };
      struct Node
      {
        int _val;
        // 注:Node*和std::shared_ptr<Node>不是同一个类型
        Joy::weak_ptr<Node> _prev;
        Joy::weak_ptr<Node> _next;
        ~Node()
        {
          cout << "~Node()" << endl;
        }
      };
      // shared_ptr的循环引用
      void SharedPtrTest5()
      {
        Joy::shared_ptr<Node> node1(new Node);
        Joy::shared_ptr<Node> node2(new Node);
        cout << node1.use_count() << endl;
        cout << node2.use_count() << endl;
        cout << "--------" << endl;
        node1->_next = node2;
        node2->_prev = node1;
        cout << node1.use_count() << endl;
        cout << node2.use_count() << endl;
        cout << "--------" << endl;
      }
    }
    

    e5472f7328284e1f8da685bcfa6939aa.png

    550d96a1d74c451484e968f66bd354ba.png


    shared_ptr 是有线程安全问题的,需要加锁保护。这部分内容等学习完线程部分再来补充。


    定制删除器


    如果不是 new 出来的对象如何通过智能指针管理呢?或者是 new[ ] 出来的对象如果通过智能指针管理呢?那么就需要给 shared_ptr 设计定制删除器来解决这个问题。


    b14f41ce5cd641ab9866ee491d76b297.png


    new 内置类型,内置类型不会去调用构造函数。new[ ] 调用的是 operator new[ ],operator new[ ] 调用的是 malloc;delete[ ] 调用的是 oprator delete[ ],operator delete[ ] 调用的是 free。因为 delete[ ] 和 delete 都是调用 free 来释放空间,所以对于内置类型不会存在太大的问题(可能一些编译器会进行是否匹配使用的检查)。而对于自定义类型,就需要看该类型有没写析构函数。如果写了析构函数,使用 new[ ] 时,VS 会在最前面多开四个字节来存储对象的个数。当你使用 delete 来释放 new[ ] 申请的空间时,就会存在释放的位置不对且析构函数少调用了的问题。因为 VS 还给你多开了四个字节的空间,来存储对象的个数。而如果你没有写析构函数,那么编译器会自动生成析构函数。编译器自己生成析构函数的话,就不会在前面多开一个字节来存储申请空间的个数,也不会去调用析构函数,所以程序就没有报错(与内置类型不报错的原因类似)。

    1c1955a038af4324bf852736cf6d4d2f.png

    f59804ac280a44e18920d7a419e2c33c.png

    template<class T>
    struct DeleteArray
    {
      void operator()(T* ptr)
      {
        cout << "delete[]:" << ptr << endl;
        delete[] ptr;
      }
    };
    template<class T>
    struct Free
    {
      void operator()(T* ptr)
      {
        cout << "free:" << ptr << endl;
        free(ptr);
      }
    };
    // 定制删除器
    void SharedPtrTest7()
    {
      std::shared_ptr<Node> n1(new Node[5], DeleteArray<Node>());
      std::shared_ptr<Node> n2(new Node);
      std::shared_ptr<int> n3(new int[5], DeleteArray<int>());
      std::shared_ptr<int> n4((int*)malloc(sizeof(int)), Free<int>());
    }
    

    54479e7baea64fb3bcc158c7e63b1080.png


    注:在拷贝构造时传给 shared_ptr 的定制删除器也可以是 lambda 表达式,传给 unique_ptr 的定制删除器是定制删除器的类型。


    f135385ab7ef4fcb8923aa5fe6b1e144.png


    我们的改造 unique_ptr 和 shared_ptr 都是在类模板里传定制删除器的类型。


    namespace Joy
    {
      template<class T>
      struct Delete
      {
        void operator()(T* ptr)
        {
          //cout << "delete:" << ptr << endl;
          delete ptr;
        }
      };
      template<class T>
      struct DeleteArray
      {
        void operator()(T* ptr)
        {
          cout << "delete[]:" << ptr << endl;
          delete[] ptr;
        }
      };
      template<class T>
      struct Free
      {
        void operator()(T* ptr)
        {
          cout << "free:" << ptr << endl;
          free(ptr);
        }
      };
      template<class T, class D = Delete<T>>
      class unique_ptr
      {
      public:
        unique_ptr(T* ptr = nullptr)
          : _ptr(ptr)
        {}
        // C++98的防拷贝做法:将拷贝构造和赋值运算符重载弄成私有,只声明不实现
        // C++11的防拷贝做法:delete
        unique_ptr(unique_ptr<T>& up) = delete;
        unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;
        ~unique_ptr()
        {
          if (_ptr) 
          {
            cout << "Delete:" << _ptr << endl;
            D()(_ptr);
          }
        }
        T& operator*()
        {
          return *_ptr;
        }
        T* operator->()
        {
          return _ptr;
        }
      private:
        T* _ptr;
      };
      template<class T, class D = Delete<T>>
      class shared_ptr
      {
      public:
        shared_ptr(T* ptr = nullptr)
          : _ptr(ptr)
          , _pCount(new int(1))
        {}
        shared_ptr(const shared_ptr<T>& sp)
          : _ptr(sp._ptr)
          , _pCount(sp._pCount)
        {
          ++(*_pCount);
        }
        void Release()
        {
          // 减减对象的计数,如果是最后一个对象,要释放资源
          if (--(*_pCount) == 0)
          {
            //cout << "Delete:" << _ptr << endl;
            D()(_ptr);
            delete _pCount;
          }
        }
        ~shared_ptr()
        {
          Release();
        }
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
          // 通过_ptr来判断管理的资源是否是同一个。如果是同一个,
          // _ptr相等,直接返回即可
          //if(this == &sp)
          if (_ptr == sp._ptr)
          {
            return *this;
          }
          Release();  // 减减被赋值对象的计数,如果是最后一个对象,要释放资源
          // 共管新资源,++计数
          _ptr = sp._ptr;
          _pCount = sp._pCount;
          ++(*_pCount);
          return *this;
        }
        T& operator*()
        {
          return *_ptr;
        }
        T* operator->()
        {
          return _ptr;
        }
        int* GetCount() const
        {
          return _pCount;
        }
        T* get() const
        {
          return _ptr;
        }
        int use_count()
        {
          return *_pCount;
        }
      private:
        T* _ptr;
        int* _pCount; // 引用计数
      };
      void SharedPtrTest8()
      {
        Joy::shared_ptr<Node, DeleteArray<Node>> n1(new Node[5]);
        Joy::shared_ptr<Node> n2(new Node);
        Joy::shared_ptr<int, DeleteArray<int>> n3(new int[5]);
        Joy::shared_ptr<int, Free<int>> n4((int*)malloc(sizeof(int)));
      }
    }
    

    c51ccd04742e415e941f5a1bfaee77d5.png


    智能指针重点内容


    bc683ea60e9b4ec58eb868c3fe502643.png

    👉C++11和boost中智能指针的关系👈


    C++ 98 中产生了第一个智能指针 auto_ptr。

    C++ boost 给出了更实用的 scoped_ptr、shared_ptr 和 weak_ptr。

    C++ TR1 引入了 shared_ptr 等。不过注意的是 TR1 并不是标准版。

    C++ 11 引入了 unique_ptr、shared_ptr 和 weak_ptr。需要注意的是 unique_ptr 对应 boost 的scoped_ptr,并且这些智能指针的实现原理是参考 boost 中的实现的。


    👉总结👈


    本篇博客主要讲解了什么是内存泄漏、内存泄漏的危害及分类、什么是智能指针和 RAII、auto_ptr、unique_ptr、shared_ptr 和 weak_ptr 以及定制删除器等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

    相关文章
    |
    6天前
    |
    存储 程序员 C++
    深入解析C++中的函数指针与`typedef`的妙用
    本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
    27 0
    |
    1月前
    |
    存储 编译器 Linux
    【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
    本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
    88 4
    |
    2月前
    |
    存储 安全 编译器
    在 C++中,引用和指针的区别
    在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
    |
    2月前
    |
    存储 C++
    c++的指针完整教程
    本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
    62 1
    |
    2月前
    |
    存储 编译器 C语言
    C++入门2——类与对象1(类的定义和this指针)
    C++入门2——类与对象1(类的定义和this指针)
    45 2
    |
    2月前
    |
    存储 安全 编译器
    【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
    【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
    |
    2月前
    |
    存储 C++ 索引
    C++函数指针详解
    【10月更文挑战第3天】本文介绍了C++中的函数指针概念、定义与应用。函数指针是一种指向函数的特殊指针,其类型取决于函数的返回值与参数类型。定义函数指针需指定返回类型和参数列表,如 `int (*funcPtr)(int, int);`。通过赋值函数名给指针,即可调用该函数,支持两种调用格式:`(*funcPtr)(参数)` 和 `funcPtr(参数)`。函数指针还可作为参数传递给其他函数,增强程序灵活性。此外,也可创建函数指针数组,存储多个函数指针。
    |
    3月前
    |
    编译器 C++
    【C++核心】指针和引用案例详解
    这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
    55 3
    |
    2月前
    |
    存储 编译器 程序员
    【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
    【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
    |
    3月前
    |
    C++
    C++(十八)Smart Pointer 智能指针简介
    智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。