• C++——智能指针


    目录

    一.为什么需要智能指针

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

    1.智能指针的原理

     2.std::auto_ptr

     3.std::unique_ptr

    4.std::shared_ptr

    三.std::shared_ptr的循环引用

    四.删除器


    1203c92d0c6c4567b4aea1e22dc67023.gif

    一.为什么需要智能指针

    下面我们先分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析Func函数中的问题。

    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. }
    9. void Func()
    10. {
    11. // 1、如果p1这里new 抛异常会如何?
    12. // 2、如果p2这里new 抛异常会如何?
    13. // 3、如果div调用这里又会抛异常会如何?
    14. int* p1 = new int;
    15. int* p2 = new int;
    16. cout << div() << endl;
    17. delete p1;
    18. delete p2;
    19. }
    20. int main()
    21. {
    22. try
    23. {
    24. Func();
    25. }
    26. catch (exception& e)
    27. {
    28. cout << e.what() << endl;
    29. }
    30. return 0;
    31. }

    说明:

    1. 如果在p1位置出现异常,那么程序还不会出现内存泄漏,因为p1的对空间还没有申请成功。
    2. 如果在p2位置出现异常,那么就会导致p1申请的空间没有来得及被释放就会跳转。导致p1申请的空间泄漏。
    3. 如果在div()处出现异常就会导致p1,p2申请的空间都得不到释放,都会出现内存泄漏。

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

    什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
    存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
    该段内存的控制,因而造成了内存的浪费。
    内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
    内存泄漏会导致响应越来越慢,最终卡死。

    内存泄漏分类:

    C/C++程序中一般我们关心两种方面的内存泄漏:

    1.堆内存泄漏(Heap leak):

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

    2.系统资源泄漏:

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

     如何避免内存泄漏:

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

    总结一下:
    内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄
    漏检测工具

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

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

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

    1. 不需要显式地释放资源。
    2. 采用这种方式,对象所需的资源在其生命期内始终保持有效。

    设计简单的RAII智能指针:

    1. template<class T>
    2. class smart_ptr
    3. {
    4. public:
    5. smart_ptr(T *ptr)
    6. :_ptr(ptr)
    7. {
    8. }
    9. ~smart_ptr()
    10. {
    11. cout << "delete _ptr" << endl;
    12. delete _ptr;
    13. }
    14. private:
    15. T* _ptr;
    16. };
    17. int div()
    18. {
    19. int a, b;
    20. cin >> a >> b;
    21. if (b == 0)
    22. throw invalid_argument("除0错误");
    23. return a / b;
    24. }
    25. void Func()
    26. {
    27. smart_ptr<int>p1( new int);
    28. smart_ptr<int>p2(new int);
    29. cout << div() << endl;
    30. }
    31. int main()
    32. {
    33. try
    34. {
    35. Func();
    36. }
    37. catch (exception& e)
    38. {
    39. cout << e.what() << endl;
    40. }
    41. return 0;
    42. }

    测试结果:

    ac61e371ed924017be31c10550aca406.png

    1.智能指针的原理

    上述的 smart_ptr 还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可
    以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其
    像指针一样去使用。

    1. template<class T>
    2. class smart_ptr
    3. {
    4. public:
    5. smart_ptr(T *ptr)
    6. :_ptr(ptr)
    7. {
    8. }
    9. T& operator*() { return *_ptr; }
    10. T* operator->() { return _ptr; }
    11. ~smart_ptr()
    12. {
    13. cout << "delete _ptr" << endl;
    14. if(_ptr)
    15. delete _ptr;
    16. }
    17. private:
    18. T* _ptr;
    19. };
    20. struct Date
    21. {
    22. int _year;
    23. int _month;
    24. int _day;
    25. };
    26. int main()
    27. {
    28. smart_ptr<int> sp1(new int);
    29. *sp1 = 10;
    30. cout << *sp1 << endl;
    31. smart_ptr sparray(new Date);
    32. // 需要注意的是这里应该是sparray.operator->()->_year = 2018;
    33. // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
    34. sparray->_year = 2018;
    35. sparray->_month = 1;
    36. sparray->_day = 1;
    37. cout << sparray->_year << ":" << sparray->_month << ":" << sparray->_day << endl;
    38. return 0;
    39. }

    e76788e9771045d895635f4362b1b956.png

    总结一下智能指针的原理:

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

     2.std::auto_ptr

    std::auto_ptr文档

    C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
    auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原
    理。

    1. template<class T>
    2. class myatuo_ptr
    3. {
    4. public:
    5. myatuo_ptr(T* ptr)
    6. :_ptr(ptr)
    7. {
    8. }
    9. //像指针一样访问
    10. T& operator*() { return *_ptr; }
    11. T* operator->() { return _ptr; }
    12. //管理权转移,一次只能有一个对象管理资源
    13. myatuo_ptr& operator=(myatuo_ptr& ptr)
    14. {
    15. //不是自己给自己赋值
    16. if (this != &ptr)
    17. {
    18. //1.先释放当前管理的资源
    19. if (_ptr)
    20. {
    21. delete _ptr;
    22. }
    23. //2.转移管理权
    24. _ptr = ptr._ptr;
    25. ptr._ptr = nullptr;
    26. }
    27. return *this;
    28. }
    29. ~myatuo_ptr()
    30. {
    31. cout << "delete _ptr" << endl;
    32. if (_ptr)
    33. delete _ptr;
    34. }
    35. private:
    36. T* _ptr;
    37. };
    38. int main()
    39. {
    40. ikun::myatuo_ptr<int> pint(new int);
    41. *pint = 100;
    42. ikun::myatuo_ptr<int> pint1(new int);
    43. pint1 = pint;
    44. cout << *pint1 << endl;
    45. return 0;
    46. }

    06be04f672d84e43a40ce1d029399122.gif

    致命缺点:指针悬空

    b35ac57cb31344fbb33fd7920ea42ab2.png

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

     3.std::unique_ptr

    C++11中开始提供更靠谱的unique_ptr。

    unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原
    理。

    1. template<class T>
    2. class myunique_ptr
    3. {
    4. public:
    5. myunique_ptr(T* ptr)
    6. :_ptr(ptr)
    7. {
    8. }
    9. //像指针一样访问
    10. T& operator*() { return *_ptr; }
    11. T* operator->() { return _ptr; }
    12. //强制不生成赋值运算符和拷贝构造
    13. myunique_ptr& operator=(myunique_ptr& ptr) = delete;
    14. myunique_ptr(myunique_ptr& ptr) = delete;
    15. ~myunique_ptr()
    16. {
    17. cout << "delete _ptr" << endl;
    18. if (_ptr)
    19. delete _ptr;
    20. }
    21. private:
    22. T* _ptr;
    23. };

    4.std::shared_ptr

    C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。

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

    原理:

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

    实现原理:

    1. template<class T>
    2. class myshare_ptr
    3. {
    4. public:
    5. myshare_ptr(T* ptr=nullptr)
    6. :_ptr(ptr),
    7. _pcount(new int(1)),
    8. {
    9. }
    10. //像指针一样访问
    11. T& operator*() { return *_ptr; }
    12. T* operator->() { return _ptr; }
    13. //拷贝构造
    14. myshare_ptr(myshare_ptr& ptr)
    15. :_ptr(ptr._ptr),
    16. _pcount(ptr._pcount),
    17. {
    18. addref();
    19. }
    20. //赋值重载运算符
    21. myshare_ptr& operator=(myshare_ptr& ptr)
    22. {
    23. //this!=&ptr
    24. if (_ptr != ptr._ptr)
    25. {
    26. release();
    27. _ptr = ptr._ptr;
    28. _pcount = ptr._pcount;
    29. ptr.addref();
    30. }
    31. }
    32. ~myshare_ptr()
    33. {
    34. release();
    35. }
    36. private:
    37. //对引用计数的++
    38. void addref()
    39. {
    40. (*_pcount)++;
    41. }
    42. //对引用计数的--
    43. void release()
    44. {
    45. (*_pcount)--;
    46. if ((*_pcount) == 0)
    47. {
    48. if (_ptr)
    49. {
    50. delete _ptr;
    51. delete _pcount;
    52. cout << "delete _ptr" << endl;
    53. }
    54. }
    55. }
    56. private:
    57. T* _ptr;
    58. int* _pcount;
    59. };
    60. }

    需要注意的是shared_ptr的线程安全分为两方面:

    1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
    2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题,智能指针访问管理的资源,不是线程安全的。

     所以我们需要对上述代码的引用计数的操作进行加锁保护:

    1. template<class T>
    2. class myshare_ptr
    3. {
    4. public:
    5. myshare_ptr(T* ptr=nullptr)
    6. :_ptr(ptr),
    7. _pcount(new int(1)),
    8. _mutex(new mutex)
    9. {
    10. }
    11. //像指针一样访问
    12. T& operator*() { return *_ptr; }
    13. T* operator->() { return _ptr; }
    14. //拷贝构造
    15. myshare_ptr(myshare_ptr& ptr)
    16. :_ptr(ptr._ptr),
    17. _pcount(ptr._pcount),
    18. _mutex(ptr._mutex)
    19. {
    20. addref();
    21. }
    22. //赋值重载运算符
    23. myshare_ptr& operator=(myshare_ptr& ptr)
    24. {
    25. //this!=&ptr
    26. if (_ptr != ptr._ptr)
    27. {
    28. release();
    29. _ptr = ptr._ptr;
    30. _pcount = ptr._pcount;
    31. _mutex = ptr._mutex;
    32. ptr.addref();
    33. }
    34. return *this;
    35. }
    36. ~myshare_ptr()
    37. {
    38. release();
    39. }
    40. private:
    41. //对引用计数的++
    42. void addref()
    43. {
    44. _mutex->lock();//加锁
    45. (*_pcount)++;
    46. _mutex->unlock();
    47. }
    48. //对引用计数的--
    49. void release()
    50. {
    51. _mutex->lock();//加锁
    52. (*_pcount)--;
    53. if ((*_pcount) == 0)
    54. {
    55. if (_ptr)
    56. {
    57. delete _ptr;
    58. delete _pcount;
    59. deletemutex = 1;
    60. }
    61. }
    62. _mutex->unlock();
    63. if (deletemutex)
    64. {
    65. delete _mutex;
    66. }
    67. }
    68. }
    69. private:
    70. T* _ptr;
    71. int* _pcount;
    72. mutex* _mutex;
    73. };

    三.std::shared_ptr的循环引用

    shared_ptr在一些特殊的场景下也会有bug,例如下列代码:

    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 n1(new ListNode);
    11. shared_ptr n2(new ListNode);
    12. n1->_next = n2;
    13. n2->_prev = n1;
    14. return 0;
    15. }

    aff38fb3ab64415782a73a78e639c5ae.png

     循环引用分析:

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

    5a7af688b8484ac89241762cb8f717c5.png解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。

    原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。

    • weak_ptr他不是常规的智能指针,不支持RAII。
    • weak_ptr支持像指针一样。
    • weak_ptr专门设计出来,辅助解决shared_ptr的循环引用问题。
    • weak_ptr可以指向资源,但是他不参与管理,不增加引用计数。

    weak_ptr实现原理:

    1. template<class T>
    2. class myweak_ptr
    3. {
    4. public:
    5. myweak_ptr()
    6. :_ptr(nullptr)
    7. {}
    8. myweak_ptr(const shared_ptr& sp)
    9. :_ptr(sp.get())
    10. {}
    11. T& operator*()
    12. {
    13. return *_ptr;
    14. }
    15. T* operator->()
    16. {
    17. return _ptr;
    18. }
    19. T* get()
    20. {
    21. return _ptr;
    22. }
    23. private:
    24. T* _ptr;
    25. };

    解决循环引用:

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

    3b3fd7fab28b4ac4b6fadca2ecddc49f.png

    四.删除器

    如果不是new出来的对象如何通过智能指针管理呢?有可能是,malloc出来的,或者是一个文件指针,又该如何处理呢,其实shared_ptr设计了一个删除器来解决这个问题。

    删除器:是一个可调用对象,让我们根据我们管理的指针的类型,来设计特定的释放函数,防止释放混乱,将可调用在构造时传智能指针。可调用对象可以是,函数指针,lambda,函数对象,仿函数。

    例如:

    1. template<class T>
    2. struct deletedate
    3. {
    4. void operator()(T*ptr)
    5. {
    6. cout << "delete[] ptr" << endl;
    7. delete[] ptr;
    8. }
    9. };
    10. template<class T>
    11. void deletemalloc(T* ptr)
    12. {
    13. cout << "free (ptr)" << endl;
    14. free(ptr);
    15. }
    16. int main()
    17. {
    18. //lambda
    19. shared_ptr ptr(fopen("test.c", "w+"), [](FILE* fptr) {fclose(fptr); cout << "fclose(fptr)" << endl; });
    20. //函数指针
    21. shared_ptr<int> ptr1((int*)malloc(4),deletemalloc<int>);
    22. //仿函数
    23. shared_ptr<int> ptr2(new int[10], deletedate<int>());
    24. //函数对象
    25. shared_ptr<int> ptr3((int*)malloc(sizeof(int)*10), function<void(int*)>(deletemalloc<int>));
    26. return 0;
    27. }

    cd15b4f1c0894067a181ae8ae0c29af5.png

    实现原理:

    1. template<class T>
    2. class myshare_ptr
    3. {
    4. public:
    5. myshare_ptr(T* ptr=nullptr)
    6. :_ptr(ptr),
    7. _pcount(new int(1)),
    8. _mutex(new mutex)
    9. {
    10. }
    11. myshare_ptr(T* ptr, function<void(T*)> func_delete)
    12. :_ptr(ptr),
    13. _pcount(new int(1)),
    14. _mutex(new mutex),
    15. _func_delete(func_delete)
    16. {
    17. }
    18. //像指针一样访问
    19. T& operator*() { return *_ptr; }
    20. T* operator->() { return _ptr; }
    21. //拷贝构造
    22. myshare_ptr(myshare_ptr& ptr)
    23. :_ptr(ptr._ptr),
    24. _pcount(ptr._pcount),
    25. _mutex(ptr._mutex),
    26. _func_delete(ptr._func_delete)
    27. {
    28. addref();
    29. }
    30. //赋值重载运算符
    31. myshare_ptr& operator=(myshare_ptr& ptr)
    32. {
    33. //this!=&ptr
    34. if (_ptr != ptr._ptr)
    35. {
    36. release();
    37. _ptr = ptr._ptr;
    38. _pcount = ptr._pcount;
    39. _mutex = ptr._mutex;
    40. _func_delete = ptr._func_delete;
    41. ptr.addref();
    42. }
    43. return *this;
    44. }
    45. ~myshare_ptr()
    46. {
    47. release();
    48. }
    49. private:
    50. //对引用计数的++
    51. void addref()
    52. {
    53. _mutex->lock();
    54. (*_pcount)++;
    55. _mutex->unlock();
    56. }
    57. //对引用计数的--
    58. void release()
    59. {
    60. _mutex->lock();
    61. (*_pcount)--;
    62. bool deletemutex = 0;
    63. if ((*_pcount) == 0)
    64. {
    65. if (_ptr)
    66. {
    67. _func_delete(_ptr);
    68. delete _pcount;
    69. deletemutex = 1;
    70. }
    71. }
    72. _mutex->unlock();
    73. if (deletemutex)
    74. {
    75. delete _mutex;
    76. }
    77. }
    78. private:
    79. T* _ptr;
    80. int* _pcount;
    81. mutex* _mutex;
    82. function<void(T*)> _func_delete = [](T* ptr) {delete ptr};//默认释放
    83. };

     

     

  • 相关阅读:
    STM32链接脚本
    Redis7入门概述
    我的大学期末网页作业 仿学校网站制作实现 HTML+CSS西北大学新闻网带psd带js
    生成对抗网络——GAN(代码+理解)
    UE Lyda项目学习 二、距离匹配 步幅适配 同步组
    Linux基础——gcc和make
    Android开发基础——Activity生命周期
    【JavaSE】类和对象——上
    MindSpore处理自定义数据集的时候报错
    基于Echarts实现可视化数据大屏物流大数据统计平台HTML模板
  • 原文地址:https://blog.csdn.net/qq_63943454/article/details/133961425