• c++ 智能指针详解


    目录

    前言

    1、为什么需要智能指针?

    2、智能指针的原理

    3、智能指针的分类

    3.1 auto_ptr

    3.2 unique_ptr

    3.3 shared_ptr


    前言

    C++11中引入了智能指针的特性,本文将详细介绍智能指针的使用。


    1、为什么需要智能指针?

    我们来看一段代码:

    1. void Func()
    2. {
    3. int* p1 = new int;
    4. int* p2 = new int;
    5. cout << div() << endl;
    6. delete p1;
    7. delete p2;
    8. }

    在这段代码中,如果在div中抛异常,很显然会造成内存泄漏。为了避免内存泄漏,我们希望有一种指针可以做到自动管理内存释放。 

    2、智能指针的原理

    RALL:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

    在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

    1. // 使用RAII思想设计的SmartPtr类
    2. template<class T>
    3. class SmartPtr {
    4. public:
    5.   SmartPtr(T* ptr = nullptr)
    6.    : _ptr(ptr)
    7.  {}
    8.   ~SmartPtr()
    9.  {
    10.     if(_ptr)
    11.       delete _ptr;
    12.  }
    13. T& operator*() {return *_ptr;}
    14. T* operator->() {return _ptr;}
    15.  private:
    16.   T* _ptr;
    17. };

     智能指针的原理:

    •  RAII特性
    • 重载operator*和opertaor->,具有像指针一样的行为。

    3、智能指针的分类

    在c++的库中定义了几种不同的智能指针,头文件为memory。在智能指针进行拷贝构造或者赋值时,会出现两个指针指向同一块内存的情况,这样在释放空间时就会出现问题。针对这个问题有三种不同的解决方法,对应c++库中的三种指针,前两种都是不成熟的做法,简单了解即可。

    3.1 auto_ptr

    C++98版本的库中就提供了auto_ptr的智能指针。

    auto_ptr的实现原理:管理权转移的思想,简单的将原指针设为空,把管理权交给新指针。

    1. auto_ptr(auto_ptr& sp)
    2. :_ptr(sp._ptr)
    3. {
    4. // 管理权转移
    5. sp._ptr = nullptr;
    6. }

    但是这样会出现很大的问题,因为有时候指针的权限已经发生了转移,但是使用指针的人并不知道,很可能造成越界访问。

    结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr。

    3.2 unique_ptr

    unique_ptr的实现原理:简单粗暴的防拷贝。

    1. unique_ptr(const unique_ptr& sp) = delete;
    2. unique_ptr& operator=(const unique_ptr& sp) = delete;

    这个指针时是不能拷贝的,所以在很多情况下不能使用。

    3.3 shared_ptr

    C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。shared_ptr的原理是通过引用计数的方来实现多个shared_ptr对象之间共享资源。

    • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
    • 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
    • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
    •  如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

    注意:

    • 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源。为了保证指向同一块资源的shared_ptr具有相同的count,count要开辟在堆上,每个shared_ptr对象中有一个int类型指针,指向相同资源的shared_ptr中的int类型指针指向相同的count。这里不能为了达到共用效果定义静态类型count,因为静态类型是给所有对象共用,指向不同资源的指针也会共用一个count。
    • 由于引用计数count是共用的,在多线程中可能会发生安全问题,所以要加锁来保护。

    简单模拟实现如下:

    1. template<class T>
    2. class shared_ptr
    3. {
    4. public:
    5. shared_ptr(T* ptr)
    6. :_ptr(ptr)
    7. , _pcount(new int(1))
    8. , _pmutex(new mutex)
    9. {}
    10. ~shared_ptr()
    11. {
    12. RealseRef();
    13. }
    14. shared_ptr(const shared_ptr& sp)
    15. :_ptr(sp._ptr)
    16. , _pcount(sp._pcount)
    17. , _pmutex(sp._pmutex)
    18. {
    19. AddRef();
    20. }
    21. shared_ptr& operator= (const shared_ptr&sp)
    22. {
    23. if (_ptr == sp._ptr)
    24. {
    25. return *this;
    26. }
    27. //先释放掉现在指向的资源
    28. RealseRef();
    29. _ptr = sp._ptr;
    30. _pmutex = sp._pmutex;
    31. _pcount = sp._pcount;
    32. AddRef();
    33. return *this;
    34. }
    35. private:
    36. void AddRef()
    37. {
    38. _pmutex->lock();
    39. (*_pcount)++;
    40. _pmutex->unlock();
    41. }
    42. void RealseRef()
    43. {
    44. _pmutex->lock();
    45. bool flag = false;
    46. if (--(*_pcount) == 0)
    47. {
    48. delete _ptr;
    49. delete _pcount;
    50. flag = true;
    51. }
    52. _pmutex->unlock();
    53. if (flag == true)
    54. {
    55. delete _pmutex;
    56. }
    57. }
    58. T* _ptr;
    59. int* _pcount;
    60. mutex* _pmutex;
    61. };

     shared_ptr的循环引用:

    在某些情况下会使用到循环引用,这时候可能会出现问题。

    1. struct ListNode
    2. {
    3. int _data;
    4. shared_ptr _prev;
    5. shared_ptr _next;
    6. ~ListNode(){ cout << "~ListNode()" << endl; }
    7. };
    8. int main()
    9. {
    10. shared_ptr node1(new ListNode);
    11. shared_ptr node2(new ListNode);
    12. node1->_next = node2;
    13. node2->_prev = node1;
    14. return 0;
    15. }

    智能指针指向的空间中也保存有智能指针,且两个智能指针指向的空间中保存的智能指针还指向对方的空间,这称为循环引用。

    • 第一步node1和node2指向AB两块空间,两块空间的引用计数都为1。
    • 第二步A空间的_next指向B空间,B空间的_prev指向A空间,引用计数变成2。
    • 第三步node1和node2析构,调用析构函数,但是并没有成功释放空间。原因是node1和node2析构后,两块空间的引用计数都减为1,因为这两块空间的_next和_prev还没有析构,A空间的_next在空间内部,想要析构A空间的_next必须先释放A空间,想要释放A空间必须让引用计数等于0,也就是必须先析构B空间的_prev,同理析构B空间的_prev的前提是释放B空间,那就要求B空间的引用计数为0,前提是析构A空间的_next。这就变成了一个鸡生蛋和蛋生鸡的问题,发生卡死。

    为了解决这个问题。C++中增加了weak_ptr。在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。

    1. struct ListNode
    2. {
    3. int _data;
    4. weak_ptr _prev;
    5. weak_ptr _next;
    6. ~ListNode(){ cout << "~ListNode()" << endl; }
    7. };

     weak_ptr只是单纯的赋值,不会使引用计数++,析构后也不负责释放空间。 weak_ptr就好比一个局外人,只起到索引的作用。

    删除器:

    析构智能指针时需要的操作并不相同,有时候传给智能指针的指针可能会一下new出来了多个值,这时候就需要delete[ ],或者传过去的指针是打开文件的指针,析构时不应该delete而是进行关闭文件操作。shared_ptr设计了一个删除器来解决这个问题。

    del是一个可调用对象,通过传入del可以自定义析构智能指针时进行的操作。

    1. template<class T>
    2. struct DelArr
    3. {
    4. void operator()(const T* ptr)
    5. {
    6. cout << "delete[]:"<
    7. delete[] ptr;
    8. }
    9. };
    10. void test_shared_ptr_deletor()
    11. {
    12. std::shared_ptr spArr(new ListNode[10], DelArr());
    13. std::shared_ptr spfl(fopen("test.txt", "w"), [](FILE* ptr){
    14. cout << "fclose:" << ptr << endl;
    15. fclose(ptr);
    16. });
    17. }

     总结

    本文主要简单介绍了智能指针的使用,希望能给大家带来帮助。江湖路远,来日方长,我们下次见~

  • 相关阅读:
    MySQL学习笔记(1)——简单操作
    Golang | Leetcode Golang题解之第147题对链表进行插入排序
    数据结构与算法------二叉树
    【二】数据库系统
    【设计模式】10、composite 组合模式
    字符串的定义和表示
    MYSQL-GAP&插入意向锁 死锁记录
    python DVWA文件上传POC练习
    计算机操作系统重点概念整理-第一章 计算机系统概述【期末复习|考研复习】
    如何在不结束tcpdump的情况下复制完整的pcap
  • 原文地址:https://blog.csdn.net/weixin_59371851/article/details/127128774