• C++智能指针


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

         下面我们先分析一下下面这段程序有没有什么 内存方面 的问题?提示一下:注意分析 MergeSort
    函数中的问题。
    1. int div()
    2. {
    3. int a, b;
    4. cin >> a >> b;
    5. if (b == 0)
    6. throw invalid_argument("除0错误");
    7. return a / b;
    8. void Func()
    9. {
    10. // 1、如果p1这里new 抛异常会如何?
    11. // 2、如果p2这里new 抛异常会如何?
    12. // 3、如果div调用这里又会抛异常会如何?
    13. int* p1 = new int;
    14. int* p2 = new int;
    15. cout << div() << endl;
    16. delete p1;
    17. delete p2;
    18. }
    19. int main()
    20. {
    21. try
    22. {
    23. Func();
    24. }
    25. catch (exception& e)
    26. {
    27. cout << e.what() << endl;
    28. }
    29. return 0;
    30. }
    问题分析:上面的问题分析出来我们发现有什么问题?

    2. 内存泄漏

    2.1 什么是内存泄漏,内存泄漏的危害

          什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
    该段内存的控制,因而造成了内存的浪费。
          内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
    1. void MemoryLeaks()
    2. {
    3.   // 1.内存申请了忘记释放
    4.  int* p1 = (int*)malloc(sizeof(int));
    5.  int* p2 = new int;
    6.  
    7.  // 2.异常安全问题
    8.  int* p3 = new int[10];
    9.  
    10.  Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
    11.  
    12.  delete[] p3;
    13. }

    2.2 内存泄漏分类

          C/C++ 程序中一般我们关心两种方面的内存泄漏:
    • 堆内存泄漏 (Heap leak)
            堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一 块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak
    • 系统资源泄漏
            指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
      掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

      2.3 如何检测内存泄漏

      • linux 下内存泄漏检测
      • windows 下使用第三方工具
      • 其他工具

      2.4如何避免内存泄漏

      1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。 ps :这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
      2. 采用 RAII 思想或者智能指针来管理资源。
      3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
      4. 出问题了使用内存泄漏工具检测。 ps :不过很多工具都不够靠谱,或者收费昂贵。
                总结一下: 内存泄漏非常常见,解决方案分为两种:1 、事前预防型。如智能指针等。 2 、事后查错型。如泄漏检测工具。

      3.智能指针的使用及原理

      3.1 RAII

            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. private:
      14. T* _ptr;
      15. };
      16. int div()
      17. {
      18. int a, b;
      19. cin >> a >> b;
      20. if (b == 0)
      21. throw invalid_argument("除0错误");
      22. return a / b;
      23. }
      24. void Func()
      25. {
      26. ShardPtr<int> sp1(new int);
      27. ShardPtr<int> sp2(new int);
      28. cout << div() << endl;
      29. }
      30. int main()
      31. {
      32. try {
      33. Func();
      34. }
      35. catch (const exception& e)
      36. {
      37. cout << e.what() << endl;
      38. }
      39. return 0;
      40. }

      3.2 智能指针的原理

           上述的 SmartPtr 还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过-> 去访问所指空间中的内容,因此: AutoPtr 模板类中还得需要将 * -> 重载下,才可让其 像指针一样去使用
      1. template<class T>
      2. class SmartPtr
      3. {
      4. public:
      5. SmartPtr(T* ptr = nullptr)
      6. : _ptr(ptr)
      7. {}
      8. ~SmartPtr()
      9. {
      10. if (_ptr)
      11. delete _ptr;
      12. }
      13. T& operator*()
      14. {
      15. return *_ptr;
      16. }
      17. T* operator->()
      18. {
      19. return _ptr;
      20. }
      21. private:
      22. T* _ptr;
      23. };
      24. struct Date
      25. {
      26. int _year;
      27. int _month;
      28. int _day;
      29. };
      30. int main()
      31. {
      32. SmartPtr<int> sp1(new int);
      33. *sp1 = 10
      34. cout << *sp1 << endl;
      35. SmartPtr<int> sparray(new Date);
      36. // 需要注意的是这里应该是sparray.operator->()->_year = 2018;
      37. // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
      38. sparray->_year = 2018;
      39. sparray->_month = 1;
      40. sparray->_day = 1;
      41. }
          总结一下智能指针的原理:
      1. RAII 特性。
      2. 重载 operator* opertaor-> ,具有像指针一样的行为。

     3.3 std::auto_ptr

           C++98 版本的库中就提供了 auto_ptr 的智能指针。下面演示的 auto_ptr 的使用及问题。
    auto_ptr 的实现原理:管理权转移的思想,下面简化模拟实现了一份 bit::auto_ptr 来了解它的原
    理:
    1. // C++98 管理权转移 auto_ptr
    2. namespace bit
    3. {
    4. template<class T>
    5. class auto_ptr
    6. {
    7. public:
    8. auto_ptr(T* ptr)
    9. :_ptr(ptr)
    10. {}
    11. auto_ptr(auto_ptr& sp)
    12. :_ptr(sp._ptr)
    13. {
    14. // 管理权转移
    15. sp._ptr = nullptr;
    16. }
    17. auto_ptr& operator=(auto_ptr& ap)
    18. {
    19. // 检测是否为自己给自己赋值
    20. if (this != &ap)
    21. {
    22. // 释放当前对象中资源
    23. if (_ptr)
    24. delete _ptr;
    25. // 转移ap中资源到当前对象中
    26. _ptr = ap._ptr;
    27. ap._ptr = NULL;
    28. }
    29. return *this;
    30. }
    31. ~auto_ptr()
    32. {
    33. if (_ptr)
    34. {
    35. cout << "delete:" << _ptr << endl;
    36. delete _ptr;
    37. }
    38. }
    39. // 像指针一样使用
    40. T& operator*()
    41. {
    42. return *_ptr;
    43. }
    44. T* operator->()
    45. {
    46. return _ptr;
    47. }
    48. private:
    49. T* _ptr;
    50. };
    51. }
    52. // 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
    53. //int main()
    54. //{
    55. // std::auto_ptr sp1(new int);
    56. // std::auto_ptr sp2(sp1); // 管理权转移
    57. //
    58. // // sp1悬空
    59. // *sp2 = 10;
    60. // cout << *sp2 << endl;
    61. // cout << *sp1 << endl;
    62. // return 0;
    63. //}

    3.4 std::unique_ptr

         C++11 中开始提供更靠谱的unique_ptr, unique_ptr 的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份 UniquePtr 来了解它的原 理:
    1. // C++11库才更新智能指针实现
    2. // C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
    3. // C++11将boost库中智能指针精华部分吸收了过来
    4. // C++11->unique_ptr/shared_ptr/weak_ptr
    5. // unique_ptr/scoped_ptr
    6. // 原理:简单粗暴 -- 防拷贝
    7. namespace bit
    8. {
    9. template<class T>
    10. class unique_ptr
    11. {
    12. public:
    13. unique_ptr(T* ptr)
    14. :_ptr(ptr)
    15. {}
    16. ~unique_ptr()
    17. {
    18. if (_ptr)
    19. {
    20. cout << "delete:" << _ptr << endl;
    21. delete _ptr;
    22. }
    23. }
    24. // 像指针一样使用
    25. T& operator*()
    26. {
    27. return *_ptr;
    28. }
    29. T* operator->()
    30. {
    31. return _ptr;
    32. }
    33. unique_ptr(const unique_ptr& sp) = delete;
    34. unique_ptr& operator=(const unique_ptr& sp) = delete;
    35. private:
    36. T* _ptr;
    37. };
    38. }
    39. //int main()
    40. //{
    41. // /*bit::unique_ptr sp1(new int);
    42. // bit::unique_ptr sp2(sp1);*/
    43. //
    44. // std::unique_ptr sp1(new int);
    45. // //std::unique_ptr sp2(sp1);
    46. //
    47. // return 0;
    48. //}

    3.5 std::shared_ptr

          C++11 中开始提供更靠谱的并且支持拷贝的shared_ptr,shared_ptr 的原理:是通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源
    1. shared_ptr 在其内部, 给每个资源都维护了着一份计数,用来记录该份资源被几个对象共
    2. 对象被销毁时 ( 也就是析构函数调用 ) ,就说明自己不使用该资源了,对象的引用计数减
      一。
    3. 如果引用计数是 0 ,就说明自己是最后一个使用该资源的对象, 必须释放该资源。
    4. 如果不是 0 ,就说明除了自己还有其他对象在使用该份资源, 不能释放该资源 ,否则其他对
      象就成野指针了。
    1. // 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
    2. namespace bit
    3. {
    4. template<class T>
    5. class shared_ptr
    6. {
    7. public:
    8. shared_ptr(T* ptr = nullptr)
    9. :_ptr(ptr)
    10. , _pRefCount(new int(1))
    11. , _pmtx(new mutex)
    12. {}
    13. shared_ptr(const shared_ptr& sp)
    14. :_ptr(sp._ptr)
    15. , _pRefCount(sp._pRefCount)
    16. , _pmtx(sp._pmtx)
    17. {
    18. AddRef();
    19. }
    20. void Release()
    21. {
    22. _pmtx->lock();
    23. bool flag = false;
    24. if (--(*_pRefCount) == 0 && _ptr)
    25. {
    26. cout << "delete:" << _ptr << endl;
    27. delete _ptr;
    28. delete _pRefCount;
    29. flag = true;
    30. }
    31. _pmtx->unlock();
    32. if (flag == true)
    33. {
    34. delete _pmtx;
    35. }
    36. }
    37. void AddRef()
    38. {
    39. _pmtx->lock();
    40. ++(*_pRefCount);
    41. _pmtx->unlock();
    42. }
    43. shared_ptr& operator=(const shared_ptr& sp)
    44. {
    45. //if (this != &sp)
    46. if (_ptr != sp._ptr)
    47. {
    48. Release();
    49. _ptr = sp._ptr;
    50. _pRefCount = sp._pRefCount;
    51. _pmtx = sp._pmtx;
    52. AddRef();
    53. }
    54. return *this;
    55. }
    56. int use_count()
    57. {
    58. return *_pRefCount;
    59. }
    60. ~shared_ptr()
    61. {
    62. Release();
    63. }
    64. // 像指针一样使用
    65. T& operator*()
    66. {
    67. return *_ptr;
    68. }
    69. T* operator->()
    70. {
    71. return _ptr;
    72. }
    73. T* get() const
    74. {
    75. return _ptr;
    76. }
    77. private:
    78. T* _ptr;
    79. int* _pRefCount;
    80. mutex* _pmtx;
    81. };
    82. // 简化版本的weak_ptr实现
    83. template<class T>
    84. class weak_ptr
    85. {
    86. public:
    87. weak_ptr()
    88. :_ptr(nullptr)
    89. {}
    90. weak_ptr(const shared_ptr& sp)
    91. :_ptr(sp.get())
    92. {}
    93. weak_ptr& operator=(const shared_ptr& sp)
    94. {
    95. _ptr = sp.get();
    96. return *this;
    97. }
    98. T& operator*()
    99. {
    100. return *_ptr;
    101. }
    102. T* operator->()
    103. {
    104. return _ptr;
    105. }
    106. private:
    107. T* _ptr;
    108. };
    109. }
    110. // shared_ptr智能指针是线程安全的吗?
    111. // 是的,引用计数的加减是加锁保护的。但是指向资源不是线程安全的
    112. // 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了
    113. // 引用计数的线程安全问题,是智能指针要处理的
    114. //int main()
    115. //{
    116. // bit::shared_ptr sp1(new int);
    117. // bit::shared_ptr sp2(sp1);
    118. // bit::shared_ptr sp3(sp1);
    119. //
    120. // bit::shared_ptr sp4(new int);
    121. // bit::shared_ptr sp5(sp4);
    122. //
    123. // //sp1 = sp1;
    124. // //sp1 = sp2;
    125. //
    126. // //sp1 = sp4;
    127. // //sp2 = sp4;
    128. // //sp3 = sp4;
    129. //
    130. // *sp1 = 2;
    131. // *sp2 = 3;
    132. //
    133. // return 0;
    134. //}
         std::shared_ptr 的线程安全问题
         通过下面的程序我们来测试 shared_ptr 的线程安全问题。需要注意的是 shared_ptr 的线程安全分
    为两方面:
    1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时
      ++ -- ,这个操作不是原子的,引用计数原来是 1 ++ 了两次,可能还是 2. 这样引用计数就错
      乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数 ++ -- 是需要加锁
      的,也就是说引用计数的操作是线程安全的。
    2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

    1. // 1.演示引用计数线程安全问题,就把AddRefCount和SubRefCount中的锁去掉
    2. // 2.演示可能不出现线程安全问题,因为线程安全问题是偶现性问题,main函数的n改大一些概率就
    3. //变大了,就容易出现了。
    4. // 3.下面代码我们使用SharedPtr演示,是为了方便演示引用计数的线程安全问题,将代码中的
    5. //SharedPtr换成shared_ptr进行测试,可以验证库的shared_ptr,发现结论是一样的。
    6. struct Date
    7. {
    8. int _year = 0;
    9. int _month = 0;
    10. int _day = 0;
    11. };
    12. void SharePtrFunc(bit::shared_ptr& sp, size_t n, mutex& mtx)
    13. {
    14. cout << sp.get() << endl;
    15. for (size_t i = 0; i < n; ++i)
    16. {
    17. // 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
    18. bit::shared_ptr copy(sp);
    19. // 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n
    20. 次,但是最终看到的结果,并一定是加了2n
    21. {
    22. unique_lock lk(mtx);
    23. copy->_year++;
    24. copy->_month++;
    25. copy->_day++;
    26. }
    27. }
    28. }
    29. int main()
    30. {
    31. bit::shared_ptr p(new Date);
    32. cout << p.get() << endl;
    33. const size_t n = 100000;
    34. mutex mtx;
    35. thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
    36. thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));
    37. t1.join();
    38. t2.join();
    39. cout << p->_year << endl;
    40. cout << p->_month << endl;
    41. cout << p->_day << endl;
    42. cout << p.use_count() << endl;
    43. return 0;
    44. }
          std::shared_ptr 的循环引用
    1. struct ListNode
    2. {
    3. int _data;
    4. shared_ptr _prev;
    5. shared_ptr _next;
    6. ~ListNode() { cout << "~ListNode()" << endl;
    7. }
    8. };
    9. int main()
    10. {
    11. shared_ptr node1(new ListNode);
    12. shared_ptr node2(new ListNode);
    13. cout << node1.use_count() << endl;
    14. cout << node2.use_count() << endl;
    15. node1->_next = node2;
    16. node2->_prev = node1;
    17. cout << node1.use_count() << endl;
    18. cout << node2.use_count() << endl;
    19. return 0;
    20. }
         循环引用分析:
    1. node1 node2 两个智能指针对象指向两个节点,引用计数变成 1 ,我们不需要手动
      delete
    2. node1 _next 指向 node2 node2 _prev 指向 node1 ,引用计数变成 2
    3. node1 node2 析构,引用计数减到 1 ,但是 _next 还指向下一个节点。但是 _prev 还指向上
      一个节点。
    4. 也就是说 _next 析构了, node2 就释放了。
    5. 也就是说 _prev 析构了, node1 就释放了。
    6. 但是 _next 属于 node 的成员, node1 释放了, _next 才会析构,而 node1 _prev 管理, _prev
      属于 node2 成员,所以这就叫循环引用,谁也不会释放。

    1. // 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
    2. // 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和
    3. _prev不会增加node1和node2的引用计数。
    4. struct ListNode
    5. {
    6. int _data;
    7. weak_ptr _prev;
    8. weak_ptr _next;
    9. ~ListNode() { cout << "~ListNode()" << endl;
    10. }
    11. };
    12. int main()
    13. {
    14. shared_ptr node1(new ListNode);
    15. shared_ptr node2(new ListNode);
    16. cout << node1.use_count() << endl;
    17. cout << node2.use_count() << endl;
    18. node1->_next = node2;
    19. node2->_prev = node1;
    20. cout << node1.use_count() << endl;
    21. cout << node2.use_count() << endl;
    22. return 0;
    23. }
           如果不是 new 出来的对象如何通过智能指针管理呢?其实 shared_ptr 设计了一个删除器来解决这 个问题。
    1. // 仿函数的删除器
    2. template<class T>
    3. struct FreeFunc
    4. {
    5. void operator()(T* ptr)
    6. {
    7. cout << "free:" << ptr << endl;
    8. free(ptr);
    9. }
    10. };
    11. template<class T>
    12. struct DeleteArrayFunc
    13. {
    14. void operator()(T* ptr)
    15. {
    16. cout << "delete[]" << ptr << endl;
    17. delete[] ptr;
    18. }
    19. };
    20. int main()
    21. {
    22. FreeFunc<int> freeFunc;
    23. std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
    24. DeleteArrayFunc<int> deleteArrayFunc;
    25. std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);

    4.C++11boost中智能指针的关系

    1. C++ 98 中产生了第一个智能指针auto_ptr。
    2. C++ boost 给出了更实用的 scoped_ptr shared_ptr weak_ptr。
    3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
    4. C++ 11 ,引入了 unique_ptr shared_ptr weak_ptr 。需要注意的是 unique_ptr 对应 boost
      scoped_ptr 。并且这些智能指针的实现原理是参考 boost 中的实现的。
  • 相关阅读:
    【PTHREAD】线程互斥与同步之读写锁
    uniapp项目实践总结(十六)自定义下拉刷新组件
    Makefile中常用的函数
    NFT Insider #72:The Sandbox 阿尔法第三季正式启动,耐克NFT已产生超过1.85亿美元的收入
    python:argparse
    面对全球新能源汽车合作发展创维汽车如何实现共赢
    nodejs+vue+java新鲜水果门店在线商城销售系统python flask django
    window下VS2022封装动态库以及调用动态库
    Python编程 字典创建map与Zip
    【ESP-IDF环境搭建,下载编译第一个程序】
  • 原文地址:https://blog.csdn.net/zhao19971014/article/details/128171058