• C++程序员修炼手册--智能指针


    一,智能指针的使用和原理

    1.1,RAII

            RAII是基于对象的生命周期来控制程序资源(如内存,网络连接,互斥量)等,在对象构造时获取对象的资源,在对象析构的时候释放资源。实际上就是把管理一个资源责任交给一个对象,这样做的好处是:不需要显式的释放资源,对象在其生命周期内一直有效。

    使用这种思想,我们可以模拟实现一个SmartPtr类实现对资源的管理。

    1. #include<iostream>
    2. using namespace std;
    3. struct Date
    4. {
    5. int _year;
    6. int _month;
    7. int _day;
    8. };
    9. namespace Etta
    10. {
    11. template<class T>
    12. class SmartPtr
    13. //这种指针无法对对象进行拷贝和赋值。
    14. //如果拷贝构造和赋值,就会重新开空间,实现深拷贝,且对相同资源做了多次管理,
    15. //不可靠且浪费资源,由此引出了auto_ptr
    16. {
    17. public:
    18. SmartPtr(T*ptr=nullptr)
    19. :_ptr(ptr)
    20. {}
    21. ~SmartPtr()
    22. {
    23. if(_ptr)
    24. {
    25. delete _ptr;
    26. }
    27. }
    28. private:
    29. T*_ptr;
    30. };
    31. void test()
    32. {
    33. //把对象成员交给类取管理,当出了作用域以后,就对类析构,释放对象资源
    34. SmartPtr<int> sp1(new int);
    35. *sp1 = 10;
    36. cout<<*sp1<<endl;
    37. SmartPtr<Date> sparray(new Date);
    38. sparray->_year = 2018;
    39. sparray->_month = 1;
    40. sparray->_day = 1;
    41. }
    42. }

    1.2,智能指针

    上述SmartPtr不能成为智能指针,因为不具有解引用,->功能,所以我们需要重载->和*才能算是一个智能指针。

    1. #include"SmartPtr.h"
    2. // auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
    3. // C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr
    4. namespace Etta
    5. {
    6. template<class T>
    7. class auto_ptr
    8. {
    9. public:
    10. auto_ptr(T*ptr=nullptr)
    11. :_ptr(ptr)
    12. {}
    13. ~auto_ptr()
    14. {
    15. if(_ptr)
    16. {
    17. delete _ptr;
    18. }
    19. }
    20. //拷贝构造和赋值、
    21. // 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
    22. // 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
    23. auto_ptr(auto_ptr<T>&ap)
    24. :_ptr(ap._ptr)
    25. {
    26. ap._ptr=nullptr;
    27. }
    28. auto_ptr<T>& operator=(const auto_ptr<T>&ap)
    29. {
    30. if(this!=&ap)
    31. {
    32. if(_ptr)
    33. {
    34. delete _ptr;
    35. }
    36. _ptr=ap._ptr;
    37. ap._ptr=nullptr;
    38. }
    39. return *this;
    40. }
    41. T*operator->()
    42. {
    43. return _ptr;
    44. }
    45. T&operator*()
    46. {
    47. return *_ptr;
    48. }
    49. private:
    50. T* _ptr;
    51. };
    52. void test_auto_ptr()
    53. {
    54. auto_ptr<Date> ap(new Date);
    55. // 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
    56. // 通过ap对象访问资源时就会出现问题。
    57. auto_ptr<Date> copy(ap);
    58. ap->_year = 2018;
    59. }
    60. }

    但是auto_ptr的赋值和拷贝构造是通过对资源的管理权转移,导致原指针悬空,所以很多公司禁止了使用这种智能指针,所以衍生出unique_ptr。

    智能指针的特性:

                    RAII特性

                    具有像指针一样的行为,支持->和*。

    1.3,unique_ptr(不支持拷贝和赋值)

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

    1. //简单粗暴的防拷贝,简化模拟实现了一份UniquePtr来了解
    2. namespace Etta
    3. {
    4. template<class T>
    5. class unique_ptr
    6. {
    7. public:
    8. // RAII
    9. unique_ptr(T* ptr)
    10. :_ptr(ptr)
    11. {}
    12. ~unique_ptr()
    13. {
    14. cout << "delete:" << _ptr << endl;
    15. delete _ptr;
    16. }
    17. // 可以像指针一样使用
    18. T& operator*()
    19. {
    20. return *_ptr;
    21. }
    22. T* operator->()
    23. {
    24. return _ptr;
    25. }
    26. unique_ptr(const unique_ptr<T>&) = delete;
    27. unique_ptr<T>operator=(const unique_ptr<T>&) = delete;
    28. private:
    29. T* _ptr;
    30. };
    31. void test_unique_ptr()
    32. {
    33. //std::unique_ptr<int> sp1(new int);
    34. // 拷贝
    35. //std::unique_ptr<int> sp2(sp1);
    36. unique_ptr<int> sp1(new int);
    37. // 拷贝
    38. //bit::unique_ptr<int> sp2(sp1);
    39. }
    40. }

    缺点就是不支持拷贝和赋值,为了更加靠谱和支持拷贝构造,C++11提出了shared_ptr

    1.4,shared_ptr

     1, shared_ptr的原理:

            是通过引用计数的方式来实现多个shared_ptr对象之间共享资源

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

    5,因为存在计数,所以多个线程访问时,容易导致两个线程同时+,本来是3,但实际上变成2,所以为了防止这种情况,需要在拷贝和赋值的时候加锁处理。

    模拟实现:

    1. #include<thread>
    2. #include<mutex>
    3. #include<iostream>
    4. using namespace std;
    5. namespace Etta
    6. {
    7. template<class T>
    8. class share_ptr
    9. {
    10. public:
    11. share_ptr(T* ptr=nullptr)
    12. :_ptr(ptr)
    13. ,_count(new int(1))
    14. ,_mtx(new mutex)
    15. {}
    16. ~share_ptr()
    17. {
    18. ReleaseRef();
    19. }
    20. share_ptr(const share_ptr<T>&sp)
    21. :_ptr(sp._ptr)
    22. ,_mtx(sp._mtx)
    23. ,_count(sp._count)
    24. {
    25. AddRef();
    26. }
    27. //将sp赋值给this
    28. //先减去_ptr的计数,再把sp的空间给给_ptr,再对_ptr加加
    29. share_ptr<int>&operator=(const share_ptr<T>&sp)
    30. {
    31. if(_ptr!=sp._ptr)
    32. {
    33. ReleaseRef();
    34. _ptr=sp._ptr;
    35. _mtx=sp._mtx;
    36. _count=sp._count;
    37. AddRef();
    38. }
    39. return *this;
    40. }
    41. T* get() const
    42. {
    43. return _ptr;
    44. }
    45. T&operator*()
    46. {
    47. return *_ptr;
    48. }
    49. T*operator->()
    50. {
    51. return _ptr;
    52. }
    53. int use_count()
    54. {
    55. return *_count;
    56. }
    57. private:
    58. T *_ptr;
    59. mutex* _mtx;
    60. int *(_count);
    61. void AddRef()
    62. {
    63. //为什么要加锁,因为不加锁,在两个线程同时拷贝一个对象的话,如果都进来++、
    64. //容易导致产生加了2次,但实际上值为2
    65. //产生线程错误
    66. _mtx->lock();
    67. ++(*_count);
    68. _mtx->unlock();
    69. }
    70. void ReleaseRef()
    71. {
    72. _mtx->lock();
    73. bool flag=false;
    74. if(--(*_count)==0)
    75. {
    76. cout << "delete:" << _ptr << endl;
    77. delete _ptr;
    78. delete _count;
    79. flag=true;
    80. }
    81. _mtx->unlock();
    82. if(flag==true)
    83. {
    84. delete _mtx;
    85. }
    86. }
    87. };
    88. }

    2,循环引用问题

    先看这个问题:我们使用shared_ptr对节点进行管理。

    看看跑起来结果怎么样。

    1. struct ListNode
    2. {
    3. std::shared_ptr<ListNode> _next;
    4. std::shared_ptr<ListNode> _prev;
    5. int _val;
    6. ~ListNode()
    7. {
    8. cout << "~ListNode()"<<endl;
    9. }
    10. };
    11. void test_shared_ptr_cycle()
    12. {
    13. std::share_ptr<ListNode>node1(new ListNode);
    14. std::share_ptr<ListNode>node2(new ListNode);
    15. cout << node1.use_count() <<endl;
    16. cout << node2.use_count() << endl;
    17. node1->_next=node2;
    18. node2->_prev=node1;
    19. cout << node1.use_count() <<endl;
    20. cout << node2.use_count() << endl;
    21. }

    可以看到程序会崩掉,在最后没有调用ListNode的析构,导致这两个节点未被释放,

    此时,

    解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
    原理就是:node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数

    1.5,weak_ptr

    不参与资源管理,不增加shared_ptr管理资源的引用计数,可以像指针一样使用。

    1. // 不参与资源管理,不增加shared_ptr管理资源的引用计数,可以像指针一样使用
    2. template<class T>
    3. class weak_ptr
    4. {
    5. public:
    6. weak_ptr()
    7. :_ptr(nullptr)
    8. {}
    9. weak_ptr(const shared_ptr<T>& sp)
    10. :_ptr(sp.get())
    11. {}
    12. weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    13. {
    14. _ptr = sp.get();
    15. return *this;
    16. }
    17. // 可以像指针一样使用
    18. T& operator*()
    19. {
    20. return *_ptr;
    21. }
    22. T* operator->()
    23. {
    24. return _ptr;
    25. }
    26. private:
    27. T* _ptr;
    28. };

    二,总结

    智能指针主要有以下思想:

    RAII:  RAII是基于对象的生命周期来控制程序资源,构造的时候管理,析构的时候释放。

    shared_ptr:对指针的拷贝和赋值通过一个int*(count)进行++计数,析构--,加锁控制线程安全。

    weak_ptr;对象内成员不参与资源的管理,只是作为一个指针使用。

  • 相关阅读:
    mysql的代理工具实现读写分离实战
    安卓应用程序打包时超过64K限制会出现什么问题?如何解决这个问题?
    小程序的入门
    智能AI创作系统ChatGPT商业运营版源码+AI绘画系统/支持GPT联网提问/支持Midjourney绘画+Prompt应用+支持国内AI提问模型
    Python —— pytest框架
    基于支持向量机SVM和MLP多层感知神经网络的数据预测matlab仿真
    零基础真的可以学习Python吗?答案是:可以!
    步入式老化房采集模拟量电流电压
    视频号要对标内容
    Ubuntu16.04安装ukylin优麒麟系统版微信WeChat
  • 原文地址:https://blog.csdn.net/m0_63111921/article/details/125421972