• C++智能指针


    目录

    一、为什么要有智能指针?

    二、说一下内存泄露?

    1.什么是内存泄露,内存泄露的危害

    2.内存泄露的分类

    3.如何避免内存泄露?

    三、智能指针的基本原理?

    1.RAII思想

    2.智能指针的原理

    四、各种智能指针的介绍,基本实现原理以及存在的缺陷?

    1.C++98当中的auto_ptr的智能指针。

    2.C++11提供的unique_ptr

    3.C++11中的shared_ptr

    五、智能指针的更新迭代


    一、为什么要有智能指针?

    C++没有内存回收的机制,所以我们开辟出来的空间需要自己手动的进行释放。

    有时申请的空间我们会忘记释放,也有时会存在doubiefree的问题存在。

    如果malloc/new出来的空间没有进行free/delete就会造成内存泄露的问题。

    二、说一下内存泄露?

    1.什么是内存泄露,内存泄露的危害

    什么是内存泄露:内存泄露是指因为疏忽或者错误造成程序未能释放已经不再使用的内存的情况。内存泄露并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成内存的浪费。

    内存泄露的危害:长期运行的程序出现内存泄露,影响很大,如操作系统、后台服务等等,出现内存泄露会导致响应越来越慢,最终卡死。

    2.内存泄露的分类

    堆内存泄露:

    堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一块内存, 用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那 么以后这部分空间将无法再被使用,就会产生 Heap Leak

    系统资源泄露:

    指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统 资源的浪费,严重可导致系统效能减少,系统执行不稳定。

    3.如何避免内存泄露?

    • 工程前期良好的设计规范,养成良好的编码习惯,生气的内存及时释放
    • 采用RAII思想或者智能指针来管理资源
    • 出了问题可以使用内存泄露工具检测

    总结:(1)事前预防型,智能指针等。(2)事后查错型,泄露检测工具

    三、智能指针的基本原理?

    1.RAII思想

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

    在对象构造时获取资源,接着控制资源的访问使之在对象的生命周期内时钟保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理亦非资源的责任托管给了一个对象。这种做法有两大好处:(1)不需要显式的释放资源(2)采用这种方式,对象所需的资源在其生命期内时钟保持有效。

    2.智能指针的原理

    (1)RAII特性:在构造函数中进行资源的申请,析构函数中进行资源的释放

    (2)指针的特性:重载operator*和operator->,具有指针一样的行为。

    四、各种智能指针的介绍,基本实现原理以及存在的缺陷?

    1.C++98当中的auto_ptr的智能指针。

    实现原理:管理权限转移的思想(它的构造函数和赋值运算符的重载实现了管理权限转移)

    1. // 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
    2. // 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
    3. AutoPtr(AutoPtr& ap)
    4. : _ptr(ap._ptr)
    5. {
    6. ap._ptr = NULL;
    7. }
    8. AutoPtr& operator=(AutoPtr& ap)
    9. {
    10. // 检测是否为自己给自己赋值
    11. if (this != &ap)
    12. {
    13. // 释放当前对象中资源
    14. if (_ptr)
    15. delete _ptr;
    16. // 转移ap中资源到当前对象中
    17. _ptr = ap._ptr;
    18. ap._ptr = NULL;
    19. }
    20. return *this;
    21. }
    22. T& operator*() { return *_ptr; }
    23. T* operator->() { return _ptr; }
    24. private:
    25. T* _ptr;
    26. };

    存在的问题:

    1. int main()
    2. {
    3. AutoPtr ap(new Date);
    4. // 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
    5. // 通过ap对象访问资源时就会出现问题。
    6. AutoPtr copy(ap);
    7. ap->_year = 2018;
    8. return 0;
    9. }

    我们在使用ap来构造copy之后,如果再次使用ap访问对象的资源时就会出现问题。

    2.C++11提供的unique_ptr

    实现原理:防拷贝

    模拟实现

    1. class UniquePtr
    2. {
    3. public:
    4. UniquePtr(T * ptr = nullptr)
    5. : _ptr(ptr)
    6. {}
    7. ~UniquePtr()
    8. {
    9. if (_ptr)
    10. delete _ptr;
    11. }
    12. T& operator*() { return *_ptr; }
    13. T* operator->() { return _ptr; }
    14. private:
    15. // C++98防拷贝的方式:只声明不实现+声明成私有
    16. UniquePtr(UniquePtr const &);
    17. UniquePtr & operator=(UniquePtr const &);
    18. // C++11防拷贝的方式:delete
    19. UniquePtr(UniquePtr const &) = delete;
    20. UniquePtr & operator=(UniquePtr const &) = delete;
    21. private:
    22. T * _ptr;
    23. };

    3.C++11中的shared_ptr

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

    内部实现:

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

    模拟实现:

    1. template <class T>
    2. class SharedPtr
    3. {
    4. public:
    5. SharedPtr(T* ptr = nullptr)
    6. : _ptr(ptr)
    7. , _pRefCount(new int(1))
    8. , _pMutex(new mutex)
    9. {}
    10. ~SharedPtr() { Release(); }
    11. SharedPtr(const SharedPtr& sp)
    12. : _ptr(sp._ptr)
    13. , _pRefCount(sp._pRefCount)
    14. , _pMutex(sp._pMutex)
    15. {
    16. AddRefCount();
    17. }
    18. // sp1 = sp2
    19. SharedPtr& operator=(const SharedPtr& sp)
    20. {
    21. //if (this != &sp)
    22. if (_ptr != sp._ptr)
    23. {
    24. // 释放管理的旧资源
    25. Release();
    26. // 共享管理新对象的资源,并增加引用计数
    27. _ptr = sp._ptr;
    28. _pRefCount = sp._pRefCount;
    29. _pMutex = sp._pMutex;
    30. AddRefCount();
    31. }
    32. return *this;
    33. }
    34. T& operator*() { return *_ptr; }
    35. T* operator->() { return _ptr; }
    36. int UseCount() { return *_pRefCount; }
    37. T* Get() { return _ptr; }
    38. void AddRefCount()
    39. {
    40. // 加锁或者使用加1的原子操作
    41. _pMutex->lock();
    42. ++(*_pRefCount);
    43. _pMutex->unlock();
    44. }
    45. private:
    46. void Release()
    47. {
    48. bool deleteflag = false;
    49. // 引用计数减1,如果减到0,则释放资源
    50. _pMutex.lock();
    51. if (--(*_pRefCount) == 0)
    52. {
    53. delete _ptr;
    54. delete _pRefCount;
    55. deleteflag = true;
    56. }
    57. _pMutex.unlock();
    58. if (deleteflag == true)
    59. delete _pMutex;
    60. }
    61. private:
    62. int* _pRefCount; // 引用计数
    63. T* _ptr; // 指向管理资源的指针
    64. mutex* _pMutex; // 互斥锁
    65. };

     shared_ptr存在线程安全的问题

    1.智能指针对象中引用计数时多个智能指针对象共享的,两个线程中智能指针的引用计数器同时++或--操作,这个操作并不是原子性的,假设引用技术原来是1,++了两次,可能还是1,这样引用计数器就错乱了。会导致资源未释放或者程序崩溃的问题。所以智能指针中引用计数++,--操作是需要加锁的,也就是说引用计数的操作是线程安全的。

    2.智能指针管理的对象放在堆上,两个线程同时去访问会导致线程安全的问题。

    shared_ptr的循环引用

    1. #include
    2. #include
    3. using namespace std;
    4. struct ListNode
    5. {
    6. int _data;
    7. shared_ptr _prev;
    8. shared_ptr _next;
    9. ~ListNode()
    10. {
    11. cout << "~ListNode()" << endl;
    12. }
    13. };
    14. int main()
    15. {
    16. shared_ptr node1(new ListNode);
    17. shared_ptr node2(new ListNode);
    18. cout << node1.use_count() << endl;
    19. cout << node2.use_count() << endl;
    20. node1->_next = node2;
    21. node2->_prev = node1;
    22. cout << node1.use_count() << endl;
    23. cout << node2.use_count() << endl;
    24. return 0;
    25. }

    运行结果:

     由上面的运行结果我们可以知道:node1和node2都没有调用析构函数,这是为什么?

    循环引用分析:双方都在等对方先释放

    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成员,所以这就叫循环引用,谁也不会释放

     循环引用的解决方案:

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

    1. // 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
    2. // 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加
    3. node1和node2的引用计数。
    4. struct ListNode
    5. {
    6. int _data;
    7. weak_ptr _prev;
    8. weak_ptr _next;
    9. ~ListNode(){ cout << "~ListNode()" << endl; }
    10. };
    11. int main()
    12. {
    13. shared_ptr node1(new ListNode);
    14. shared_ptr 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. return 0;
    22. }

    五、智能指针的更新迭代

    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 中的实现的。
  • 相关阅读:
    聊聊x86计算机启动发生的事?
    我把一个json格式的数据读到dataframe里面了 怎么解析出自己需要的字段呢?
    代码随想录算法训练营004| 19.删除链表的倒数第N个节点,24. 两两交换链表中的节点,142.环形链表II,面试题 02.07. 链表相交
    AMCL代码详解(七)amcl中的kd-Tree
    leetcode做题笔记173. 二叉搜索树迭代器
    李宏毅:Life Long Learning
    linux centos Python + Selenium+Chrome自动化测试环境搭建?
    【Android】-- 按钮(复选框CheckBox、开关按钮Switch、单选按钮RadioButton)
    网站服务器怎么部署
    Java读取单个大文件的json数据避免内存溢出
  • 原文地址:https://blog.csdn.net/qq_57822158/article/details/126026837