小言_互联网的博客

【 C++ 】智能指针

337人阅读  评论(0)

目录

1、内存泄漏

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

        内存泄漏分类

        如何检测内存泄漏(了解)

        如何避免内存泄漏

2、为什么需要智能指针

        智能指针的使用及原理

        RAII(智能指针指导思想)

        智能指针的浅拷贝问题

3、C++库里的智能指针

        3.1、std::auto_ptr(不推荐)

        3.2、std::unique_ptr

        3.3、std::shared_ptr

                 shared_ptr的设计原理

                 shared_ptr的线程安全问题

        3.4、std::weak_ptr

                 shared_ptr的循环引用问题

                 weak_ptr解决shared_ptr的循环引用问题

4、定制删除器

        unique_ptr中的定制删除器

        shared_ptr的定制删除器

5、C++11和boost中智能指针的关系


1、内存泄漏

在正式讲解智能指针前,先来回顾下内存泄漏的相关知识点


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

什么是内存泄漏:

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

内存泄漏的危害:

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

   
  1. void MemoryLeaks()
  2. {
  3. // 1.内存申请了忘记释放
  4. int* p1 = ( int*) malloc( sizeof( int));
  5. int* p2 = new int;
  6. // 2.异常安全问题
  7. int* p3 = new int[ 10];
  8. Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
  9. delete[] p3;
  10. }

内存泄漏分类

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

堆内存泄漏(Heap leak)

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

系统资源泄漏

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

如何检测内存泄漏(了解)


如何避免内存泄漏

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

总结一下:

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

2、为什么需要智能指针

根据上面的学习我们得知内存泄漏是指因为疏忽或者错误,造成程序未能释放已经不再使用的内存的情况,如下就是一个典型的例子:


   
  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. int* p1 = new int[ 10];
  12. int* p2 = new int[ 10];
  13. cout << div() << endl;
  14. delete[] p1;
  15. delete[] p2;
  16. }
  17. int main()
  18. {
  19. try
  20. {
  21. Func();
  22. }
  23. catch (exception& e)
  24. {
  25. cout << e. what() << endl;
  26. }
  27. return 0;
  28. }
  • 当b=0时,发生除0错误,此时div函数会抛一个异常,会直接跳到main函数的catch进行捕获,这就完美的错过了先前new出的p1和p2的delete释放,继而产生内存泄漏。为了解决此问题,我们推出以下两种方法

1、异常的重新捕获

  • 我们可以直接在Func函数中对div函数抛出的异常直接进行捕获,在catch内部捕获时释放先前new出来的p1、p2俩资源,随后再将捕获到的异常重新抛出即可:

   
  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. int* p1 = new int[ 10];
  12. int* p2 = new int[ 10];
  13. try
  14. {
  15. cout << div() << endl;
  16. }
  17. catch (...)
  18. {
  19. delete[] p1;
  20. delete[] p2;
  21. throw;
  22. }
  23. delete[] p1;
  24. delete[] p2;
  25. }
  26. int main()
  27. {
  28. try
  29. {
  30. Func();
  31. }
  32. catch (exception& e)
  33. {
  34. cout << e. what() << endl;
  35. }
  36. return 0;
  37. }

仔细看上面这段程序,难道我使用异常的重新捕获就能解决内存泄漏的问题吗?

  • 当p1抛异常时,会直接跳出去进行捕获,此时还没有new资源成功,也不需要释放,没有任何问题
  • 当p2抛异常时,会出现问题,p2抛异常会直接跳出去捕获,此时又会出现new出来的资源p1未能释放造成内存泄漏
  • 上述写的异常的重新捕获仅是针对于p2不抛异常时,针对于div抛异常而造成p1和p2未释放所作的处理,一旦p2抛了异常,根本就不会走到后续的delete那一步,直接跳出去进行捕获,从而造成p1内存泄漏
  • 可能有人说我把p2放到try{}catch(){}里面呢,这也是不可取的,因为p2抛异常时,p2还没new出来呢,还没申请资源成功怎么能进行后续的delete[] p2呢,这么做同样会有问题。何况这还只是p1和p2,如果有p3、p4……呢?你怎么知道会是哪个new抛异常呢??

由此可见,使用异常的重新捕获并不能解决所有情况,至此,我们推出智能指针的方法。

2、智能指针:

  • 至于智能指针是什么以及怎么使用智能指针还请看下文:

智能指针的使用及原理

RAII(智能指针指导思想)

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

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

这种做法有两大好处:

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

如下我们来使用RAII的思想设计一个智能指针SmartPtr


   
  1. namespace cpp
  2. {
  3. template< class T>
  4. class SmartPtr
  5. {
  6. public:
  7. //RAII思想
  8. SmartPtr(T* ptr)
  9. :_ptr(ptr)
  10. {}
  11. ~ SmartPtr()
  12. {
  13. cout << "delete: " << _ptr << endl; //方便观察谁释放
  14. delete _ptr;
  15. }
  16. //像指针一样使用
  17. T& operator*()
  18. {
  19. return *_ptr;
  20. }
  21. T* operator->()
  22. {
  23. return _ptr;
  24. }
  25. T* Get()
  26. {
  27. return _ptr;
  28. }
  29. private:
  30. T* _ptr;
  31. };
  32. }

注意我上面还实现了*和->的运算符重载,这样做是为了让其对象能够像指针一样使用,效果如下:


   
  1. void Func()
  2. {
  3. cpp::SmartPtr<int> sp1(new int);
  4. cpp::SmartPtr<pair<string, int>> sp2( new pair<string, int>( "sort", 1));
  5. *sp1 = 0;
  6. sp2->second = 10;
  7. sp2->first = "ten";
  8. }

现在实现好了智能指针,上述程序就不会出现内存泄漏了,调整后的代码如下:


   
  1. double div()
  2. {
  3. double 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. cpp::SmartPtr<int> sp1(new int);
  12. cpp::SmartPtr<int> sp2(new int);
  13. cpp::SmartPtr<int> sp3(new int);
  14. cpp::SmartPtr<int> sp4(new int);
  15. cout << div() << endl;
  16. }
  17. int main()
  18. {
  19. try
  20. {
  21. Func();
  22. }
  23. catch (exception& e)
  24. {
  25. cout << e. what() << endl;
  26. }
  27. return 0;
  28. }

正常情况下:

发生除0错误时:

  • 此时这个程序发生除0错误时会抛异常,跳到main函数进行捕获,但此时也会出了new资源的生命周期,自动调用托管给的智能指针的析构函数来进行释放,不会出现内存泄漏问题。
  • 当任何一个new出来的资源抛异常时,都会跳出去进行捕获,同样是先前new出来的资源出了生命周期,自动调用托管给的智能指针的析构函数来进行释放,而后面的还没new,自然不用处理,也不会出现内存泄漏问题。

智能指针的浅拷贝问题

仔细看我上面实现的智能指针,有一个很大的问题:如何拷贝?看如下的代码:


   
  1. int main()
  2. {
  3. cpp::SmartPtr<int> sp1(new int);
  4. cpp::SmartPtr<int> sp2(sp1); //拷贝构造
  5. cpp::SmartPtr<int> sp3(new int);
  6. cpp::SmartPtr<int> sp4(new int);
  7. sp3 = sp4; //拷贝赋值
  8. return 0;
  9. }

此时我要拿sp1拷贝给sp2完成拷贝构造,或者是拿sp3赋值给sp3都会存在问题,导致程序崩溃,原因如下:

  • 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此sp1拷贝sp2后,相当于sp1和sp2管理了同一块内存空间,所以当sp1和sp2析构时就会导致这块空间被释放两次,程序崩溃。

  • 类似的,把sp4赋值给sp3时,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放。

需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是为了让这两个指针指向同一块内存空间,所以这里本就应该时浅拷贝(和迭代器那块有点像,也是浅拷贝),迭代器浅拷贝没问题的原因在于其不释放节点,而智能指针的资源是托管给我的,需要释放资源,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同类型的智能指针。详情见下文。


3、C++库里的智能指针

3.1、std::auto_ptr(不推荐)

std::auto_ptr文档

auto_ptr的实现原理:管理权转移的思想

  • auto_ptr是C++98引入的智能指针,其通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这样同一个资源就不会被多次释放了,示例如下:

   
  1. int main()
  2. {
  3. std::auto_ptr<int> ap1(new int(1));
  4. std::auto_ptr<int> ap2(ap1);
  5. //*ap1 = 10;//ap1悬空err
  6. return 0;
  7. }

下面我将通过调试的方法来演示管理权转移:

  • 但一个对象的管理权转移也就意味着,该对象不能再用原来管理的资源进行访问了,否则程序就会崩溃,会导致被拷贝对象(sp1)悬空。因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题,很多公司也都明确规定了禁止使用auto_ptr。下面就来模拟实现一下。

auto_ptr的模拟实现

  1. 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源
  2. 对*和->运算符进行重载,使auto_ptr具有像指针一样的行为
  3. 在拷贝构造函数中,用传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空
  4. 在拷贝赋值函数中,先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空

   
  1. //auto_ptr的模拟实现
  2. namespace cpp
  3. {
  4. template< class T>
  5. class auto_ptr
  6. {
  7. public:
  8. //构造函数
  9. auto_ptr(T* ptr)
  10. :_ptr(ptr)
  11. {}
  12. //析构函数
  13. ~ auto_ptr()
  14. {
  15. if (_ptr != nullptr)
  16. {
  17. cout << "delete: " << _ptr << endl; //方便观察谁释放
  18. delete _ptr;
  19. _ptr = nullptr;
  20. }
  21. }
  22. //拷贝构造函数 sp2(sp1)
  23. auto_ptr(auto_ptr<T>& ap)
  24. :_ptr(ap._ptr)
  25. {
  26. ap._ptr = nullptr;
  27. }
  28. //拷贝赋值函数
  29. auto_ptr& operator=(auto_ptr<T>& ap)
  30. {
  31. if ( this != &ap)
  32. {
  33. delete _ptr; //释放自己管理的资源
  34. _ptr = ap._ptr; //接管ap对象的资源
  35. ap._ptr = nullptr; //管理权转移后ap被置空
  36. }
  37. return * this;
  38. }
  39. //*运算符重载
  40. T& operator*()
  41. {
  42. return *_ptr;
  43. }
  44. //->运算符重载
  45. T* operator->()
  46. {
  47. return _ptr;
  48. }
  49. T* get()
  50. {
  51. return _ptr;
  52. }
  53. private:
  54. T* _ptr;
  55. };
  56. }

3.2、std::unique_ptr

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

unique_ptr文档

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

  • C++11引入的unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,如果强行拷贝,那么编译就会报错,这样也能保证资源不会被多次释放。比如:

   
  1. int main()
  2. {
  3. std::unique_ptr<int> up1(new int);
  4. //std::unique_ptr<int> up2(up1);err,不能拷贝
  5. return 0;
  6. }

unique_ptr的模拟实现:

  1. 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源
  2. 对*和->运算符进行重载,使auto_ptr具有像指针一样的行为
  3. 用C++98的方式把拷贝构造函数和拷贝赋值函数只声明不实现,并且声明为私有,或者用C++11的方式在这俩函数后面加=delete,从而防止外部调用。

   
  1. //unique_ptr的模拟实现
  2. namespace cpp
  3. {
  4. template< class T>
  5. class unique_ptr
  6. {
  7. public:
  8. //构造函数
  9. unique_ptr(T* ptr)
  10. :_ptr(ptr)
  11. {}
  12. //析构函数
  13. ~ unique_ptr()
  14. {
  15. if (_ptr != nullptr)
  16. {
  17. cout << "delete: " << _ptr << endl; //方便观察谁释放
  18. delete _ptr;
  19. _ptr = nullptr;
  20. }
  21. }
  22. //*运算符重载
  23. T& operator*()
  24. {
  25. return *_ptr;
  26. }
  27. //->运算符重载
  28. T* operator->()
  29. {
  30. return _ptr;
  31. }
  32. T* get()
  33. {
  34. return _ptr;
  35. }
  36. //法一:C++11
  37. unique_ptr( const unique_ptr<T>& up) = delete; //删除函数
  38. unique_ptr<T>& operator=( const unique_ptr<T>& up) = delete;
  39. //法二:C++98
  40. /*private:
  41. //1、只声明,不实现
  42. //2、声明成私有
  43. unique_ptr(unique_ptr<T>& up);//拷贝构造
  44. unique_ptr& operator=(unique_ptr<T>& up);//拷贝赋值*/
  45. private:
  46. T* _ptr;
  47. };
  48. }

通过对unique_ptr和auto_ptr的讲解,我们得知auto_ptr虽允许拷贝,但会存在悬空的风险,有隐患,unique_ptr没有这样的问题,但是unique_ptr的功能不全,不能实现拷贝的相关功能,但是总有一些场景是需要用到拷贝的,鉴于此,我们来看下面的shared_ptr


3.3、std::shared_ptr

shared_ptr的设计原理

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

std::shared_ptr文档

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

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

通过引用计数的方式就能至此多个对象一起管理某个资源,也就支持了智能指针的拷贝,并且也只有当一个资源的引用计数减到0时才会释放资源,从而保证同一个资源不会释放多次。


   
  1. int main()
  2. {
  3. std::shared_ptr<int> sp1(new int);
  4. std::shared_ptr<int> sp2(sp1);
  5. std::shared_ptr< int> sp3 = sp2;
  6. return 0;
  7. }

shared_ptr的模拟实现:

  1. 在shared_ptr类中定义一个int*类型的成员变量_pCount在堆上,表示智能指针对象管理的资源对应的引用计数
  2. 单独写个Release释放函数,用于处理释放资源和计数的函数,将管理资源对应的引用计数--,当减到0时释放资源和计数,便于后续析构函数和拷贝赋值函数的复用
  3. 在析构函数中直接复用Release释放函数
  4. 在构造函数中获取资源,并把引用计数设为1,表示当前仅有一个资源在管理此资源
  5. 在拷贝构造函数中,一同管理传入对象的资源和计数,并且++计数,每拷贝一次,就++计数一次
  6. 在拷贝赋值函数中,先将当前对象管理的资源对应的计数--,如果减到0就要释放此资源(这个步骤可以复用Release函数),然后一同管理传入对象的资源和计数,同时对应的计数++,注意(管理同一块资源的对象之间不能进行赋值操作)
  7. 对*和->运算符进行重载,使其可以像指针一样使用

   
  1. //shared_ptr的模拟实现
  2. namespace cpp
  3. {
  4. template< class T>
  5. class shared_ptr
  6. {
  7. public:
  8. //释放函数
  9. void Release()
  10. {
  11. if (--(*_pCount) == 0 && _ptr) //每走一次析构,计数就--,直到计数为0时才释放管理的资源
  12. {
  13. cout << "delete: " << _ptr << endl; //方便观察谁释放
  14. //释放资源和计数
  15. delete _ptr;
  16. _ptr = nullptr;
  17. delete _pCount;
  18. _pCount = nullptr;
  19. }
  20. }
  21. //构造函数
  22. shared_ptr(T* ptr)
  23. :_ptr(ptr)
  24. , _pCount( new int( 1)) //构造一个资源就把对应的计数设为1
  25. {}
  26. //析构函数
  27. ~ shared_ptr()
  28. {
  29. Release();
  30. }
  31. //拷贝构造
  32. shared_ptr( const shared_ptr<T>& sp)
  33. :_ptr(sp._ptr)
  34. , _pCount(sp._pCount)
  35. {
  36. (*_pCount)++; //每拷贝一个对象就对计数++
  37. }
  38. //拷贝赋值sp1 = sp3
  39. shared_ptr<T>& operator=( const shared_ptr<T>& sp)
  40. {
  41. if (_ptr != sp._ptr) //管理同一块资源的对象之间不能进行赋值操作
  42. {
  43. Release(); //需要先--sp1的计数,因为sp1托管其它资源了,减去自己原先的计数,并且计数减到0时释放sp1之前管理的资源,统一放到Release函数处理
  44. _ptr = sp._ptr; //把sp3的资源赋给sp1
  45. _pCount = sp._pCount; //把sp3的计数赋给sp1
  46. ++(*_pCount); //此时sp3和sp1共同托管sp3的资源,相应的计数++
  47. }
  48. return * this;
  49. }
  50. //*运算符重载
  51. T& operator*()
  52. {
  53. return *_ptr;
  54. }
  55. //->运算符重载
  56. T* operator->()
  57. {
  58. return _ptr;
  59. }
  60. T* get() const
  61. {
  62. return _ptr;
  63. }
  64. int use_count()
  65. {
  66. return *_pCount;
  67. }
  68. private:
  69. T* _ptr; //管理的资源
  70. int* _pCount; //管理的资源对应的引用计数
  71. };
  72. }

下面通过调试来演示下shared_ptr完成拷贝构造和拷贝赋值的过程

拷贝构造:

拷贝赋值:

  • 注意:引用计数一定是int*类型的指针(在堆区)。下面解释原因:

首先,shared_ptr中的引用计数_pCount不能是int出来的,这会导致管理同一块资源的shared_ptr对象有不同的引用计数,而多个对象管理统一块资源的本质是其资源和引用计数变量完全一致,因此不能使用int出来的_pCount,图示如下:

shared_ptr的引用计数_pCount也不能是静态成员变量,因为静态成员变量是所有类型对象共享的,这回导致管理不同资源的对象的引用计数都是一样的,而不同资源的对象的引用计数应该是不同的。

这里只能把shared_ptr的引用计数_pCount设定为指针, 当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的指针,如果有其它对象也要管理此资源,那么资源和引用计数都要赋给他,此时管理同一个资源的多个对象访问到的就是同一个引用计数,管理不同资源的对象访问的就不是同一个引用计数,相当于将各个资源与其对应的计数进行了绑定。注意后续释放资源的时候也要把此在堆区的计数变量释放掉。


shared_ptr的线程安全问题

未完待续……

3.4、std::weak_ptr

shared_ptr的循环引用问题

shared_ptr的循环引用问题只有在特定情况下才会出现,看如下的示例(定义一个双向的节点类,在析构函数中以打日志的方式输出一条提示语句,告知我们节点有无释放,在main函数中new出p1和p2两个节点,并把p1和p2建立双向链接关系,最后delete释放俩节点)。


   
  1. struct ListNode
  2. {
  3. ListNode* _next = nullptr;
  4. ListNode* _prev = nullptr;
  5. int _val = 0;
  6. ~ ListNode()
  7. {
  8. cout << "~ListNode()" << endl;
  9. }
  10. };
  11. int main()
  12. {
  13. ListNode* p1 = new ListNode;
  14. ListNode* p2 = new ListNode;
  15. p1->_next = p2;
  16. p2->_prev = p1;
  17. delete p1;
  18. delete p2;
  19. return 0;
  20. }

此段程序是没有问题的,new出的p1和p2都能正常释放:

假设现在出现种种原因导致程序抛异常,以至于内存泄漏,我们现在需要将其放入智能指针(shared_ptr)里头,让其托管资源并帮助我们释放资源,注意要把ListNode节点类中的_prev和_next指针也改为shared_ptr类型,便于后续的节点赋值操作。


   
  1. struct ListNode
  2. {
  3. std::shared_ptr<ListNode> _next = nullptr;
  4. std::shared_ptr<ListNode> _prev = nullptr;
  5. int _val = 0;
  6. ~ ListNode()
  7. {
  8. cout << "~ListNode()" << endl;
  9. }
  10. };
  11. int main()
  12. {
  13. std::shared_ptr<ListNode> p1(new ListNode);
  14. std::shared_ptr<ListNode> p2(new ListNode);
  15. p1->_next = p2;
  16. p2->_prev = p1;
  17. return 0;
  18. }
  • 此时程序会出现一个严重的问题:内存泄漏。出错的地方在p1和p2建立链接关系那,如果我注释掉链接节点中的任意一个代码,程序都不会出现任何问题,两个节点最后都能释放,造成此现象的原因就是shared_ptr的循环引用,下面画图演示。

首先,我new了两个节点,并将这俩节点的资源托管给两个shared_ptr智能指针,此时这俩资源对应的引用计数均为1。

接着执行p1->_next = p2; p2->_prev = p1; 建立好了链接关系后,资源1的_next成员和p2一同管理资源2,资源2中的_prev成员和p1一同管理资源1,此时资源1和资源2对应的引用计数都增加到了2。

当出了main函数作用域时,p1和p2的声明周期也就结束了,节点p2和p1相继调用析构函数释放了,此时资源1和资源2的引用计数相应的--到1。

下面来解释为何会出现循环引用以至于节点未释放内存泄漏的原因:

  • 此时管理资源2的是资源1中的_next指针,_next释放,资源2才释放,类似的管理资源1的是资源2中的_prev指针,_prev释放,资源1才释放。
  • 只有资源对应的引用计数减到0时资源才会释放

总结:

  • 资源2的_prev管着左边的节点资源1,资源1的_next管着右边的节点资源2,它们分别是两个节点的自定义成员,只有节点释放,成员才析构释放。
  • 所以,左边节点资源1释放,资源2的_next才析构,左边节点的释放又依赖于右边节点资源2的_prev,_prev析构,左边节点才释放,而右边节点的_prev想要析构,又取决于右边节点的释放,而右边节点想要释放,又依赖于左边节点的_next……这不纯纯套娃吗,你缠着我,我缠着你

上述无限套娃的问题就是shared_ptr智能指针中典型的循环引用问题。但在一开始我说到p1和p2的链接关系但凡去掉一个,p1和p2就都能正常释放,下面画图演示:

  • 首先,我new了两个节点,并将这俩节点的资源托管给两个shared_ptr智能指针,此时这俩资源对应的引用计数均为1。

  • 接着执行p1->_next = p2;此时资源1的_next成员和p2一同管理资源2,资源2的引用计数为2,资源1仅是被p1管理,引用计数仍为1。

当出了main函数作用域时,p1和p2的声明周期也就结束了,节点p2和p1相继调用析构函数释放了,此时资源1和资源2的引用计数相应的--,资源1的引用计数为0,资源2的引用计数为1。

当资源1的引用计数减到0时,此节点就释放了,此时资源1的_next也就析构释放了,但是_next又管理者资源2,当资源1的_next释放了,节点2对应的资源也就释放了,相应的引用计数--到0。此时节点正常释放,截图运行结果如下:

当然实际工程中,我不可能说想要链接p1和p2还只留一个,这样何谈链接,虽然对于shared_ptr,只有这样才能避免出现循环引用问题的内存泄漏,为了解决上述出现的循环引用问题,于是库里推出了weak_ptr智能指针。


weak_ptr解决shared_ptr的循环引用问题

weak_ptr可以接收一个shared_ptr的对象。拷贝shared_ptr的对象,进行辅助管理,weak_ptr解决shared_ptr的循环引用问题的原理就是,p1->_next = p2;和p2->_prev = p1;时weak_ptr的_next和_prev不会增加p1和p2的引用计数。

我们把ListNode节点类中的_next和_prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时p1和p2节点的声明周期结束时,两个资源对应的引用计数减到0,继而可释放两个节点的资源。如下的示例:(下面的use_count函数专门用于输出当前的引用计数)


   
  1. struct ListNode
  2. {
  3. std::weak_ptr<ListNode> _next;
  4. std::weak_ptr<ListNode> _prev;
  5. int _val = 0;
  6. ~ ListNode()
  7. {
  8. cout << "~ListNode()" << endl;
  9. }
  10. };
  11. int main()
  12. {
  13. std::shared_ptr<ListNode> p1(new ListNode);
  14. std::shared_ptr<ListNode> p2(new ListNode);
  15. cout << p1. use_count() << endl;
  16. cout << p2. use_count() << endl;
  17. p1->_next = p2;
  18. p2->_prev = p1;
  19. cout << p1. use_count() << endl;
  20. cout << p2. use_count() << endl;
  21. return 0;
  22. }

  • 通过use_count函数获取的这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数都是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数。由此可见,weak_ptr解决了shared_ptr的循环引用问题。

weak_ptr的模拟实现:

  • 提供一个无参的构造函数
  • 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时仅获取shared_ptr对象管理的资源
  • 支持用shared_ptr对象拷贝赋值weak_ptr对象,赋值时仅获取shared_ptr对象管理的资源
  • 对*和->运算符重载,使weak_ptr可以像指针一样使用

   
  1. //weak_ptr的模拟实现
  2. namespace cpp
  3. {
  4. template< class T>
  5. class weak_ptr
  6. {
  7. public:
  8. //无参构造函数
  9. weak_ptr()
  10. :_ptr( nullptr)
  11. {}
  12. //用shared_ptr对象拷贝构造weak_ptr对象
  13. weak_ptr( const shared_ptr<T>& sp)
  14. :_ptr(sp. get())
  15. {}
  16. //用shared_ptr对象拷贝赋值weak_ptr对象
  17. weak_ptr<T>& operator=( const shared_ptr<T>& sp)
  18. {
  19. if (_ptr != sp. get())
  20. {
  21. _ptr = sp. get();
  22. }
  23. return * this;
  24. }
  25. //*运算符重载
  26. T& operator*()
  27. {
  28. return *_ptr;
  29. }
  30. //->运算符重载
  31. T* operator->()
  32. {
  33. return _ptr;
  34. }
  35. private:
  36. T* _ptr; //管理的资源
  37. };
  38. }

总结:weak_ptr不参与指向资源的释放管理,weak_ptr的意义在于解决shared_ptr的循环引用问题。


4、定制删除器

上述我们讲解的unique_ptr和shared_ptr都会面临一个巨大的问题,上述所有的智能指针都是默认以delete _ptr的方式进行释放资源,但是智能指针并不只是管理new出来的资源,万一我是new [ ]、malloc、文件指针……,下面分别讨论unique_ptr和shared_ptr对应的定制删除器。


unique_ptr中的定制删除器

看如下我有三种不同方式申请的资源:


   
  1. class Date
  2. {
  3. public:
  4. ~ Date()
  5. {
  6. cout << "~Date()" << endl;
  7. }
  8. private:
  9. int _year = 1;
  10. int _month = 1;
  11. int _day = 1;
  12. };
  13. int main()
  14. {
  15. std::unique_ptr<Date> up1(new Date[10]); //err
  16. std::unique_ptr<Date> up2((Date*)malloc(sizeof(Date) * 10)); //err
  17. std::unique_ptr<FILE> up3((FILE*)fopen("Test.cpp", "r")); //err
  18. return 0;
  19. }
  • 此时当智能指针对象的声明周期结束时,再像先前那样一味的使用delete的方式释放资源就会出现程序崩溃,对于new [ ],应该用delete[ ]的方式释放,对于malloc的,应该用free的方式释放。

为了解决上述问题,C++中的unique_ptr在模板参数中以传仿函数类型的方式传入的删除器(只需掌握下图中的第一种即可):

由此可见,我们只需要对不同方式申请的资源写上对应的仿函数完成其需要的释放方式即可,如下:


   
  1. class Date
  2. {
  3. public:
  4. ~ Date()
  5. {
  6. cout << "~Date()" << endl;
  7. }
  8. private:
  9. int _year = 1;
  10. int _month = 1;
  11. int _day = 1;
  12. };
  13. //针对new[]的释放
  14. template< class T>
  15. struct DeleteArray
  16. {
  17. void operator()(T* ptr)
  18. {
  19. cout << "delete[]" << ptr << endl;
  20. delete[] ptr;
  21. }
  22. };
  23. //针对malloc的释放
  24. template< class T>
  25. struct Free
  26. {
  27. void operator()(T* ptr)
  28. {
  29. cout << "free" << ptr << endl;
  30. free(ptr);
  31. }
  32. };
  33. //针对fopen的释放
  34. struct Fclose
  35. {
  36. void operator()(FILE* ptr)
  37. {
  38. cout << "fclose" << ptr << endl;
  39. fclose(ptr);
  40. }
  41. };
  42. int main()
  43. {
  44. std::unique_ptr<Date, DeleteArray<Date>> up1( new Date[ 10]);
  45. std::unique_ptr<Date, Free<Date>> up2((Date*) malloc( sizeof(Date) * 10));
  46. std::unique_ptr<FILE, Fclose> up3((FILE*)fopen("Test.cpp", "r"));
  47. return 0;
  48. }

下面来调整我们自己实现的unique_ptr,使其支持定制删除器。

  1. C++中的unique_ptr在模板参数中以传仿函数类型的方式传入的删除器
  2. 针对于unique_ptr的析构函数,构造传入仿函数的对象,传入对应的指针,调用相应的仿函数完成对应的类型的释放即可

   
  1. //unique_ptr的完整模拟实现
  2. namespace cpp
  3. {
  4. //默认类型的释放,针对单纯的new资源的释放
  5. template< class T>
  6. struct default_delete
  7. {
  8. void operator()(T* ptr)
  9. {
  10. delete ptr;
  11. }
  12. };
  13. //针对new[]的释放
  14. template< class T>
  15. struct DeleteArray
  16. {
  17. void operator()(T* ptr)
  18. {
  19. cout << "delete[]" << ptr << endl;
  20. delete[] ptr;
  21. }
  22. };
  23. //针对malloc的释放
  24. template< class T>
  25. struct Free
  26. {
  27. void operator()(T* ptr)
  28. {
  29. cout << "free" << ptr << endl;
  30. free(ptr);
  31. }
  32. };
  33. //针对fopen的释放
  34. struct Fclose
  35. {
  36. void operator()(FILE* ptr)
  37. {
  38. cout << "fclose" << ptr << endl;
  39. fclose(ptr);
  40. }
  41. };
  42. template< class T, class D = default_delete<T>> //模板参数中传入仿函数类型
  43. class unique_ptr
  44. {
  45. public:
  46. //构造函数
  47. unique_ptr(T* ptr)
  48. :_ptr(ptr)
  49. {}
  50. //析构函数
  51. ~ unique_ptr()
  52. {
  53. if (_ptr != nullptr)
  54. {
  55. //cout << "delete: " << _ptr << endl;//方便观察谁释放
  56. //delete _ptr;
  57. D del;
  58. del(_ptr);
  59. _ptr = nullptr;
  60. }
  61. }
  62. //*运算符重载
  63. T& operator*()
  64. {
  65. return *_ptr;
  66. }
  67. //->运算符重载
  68. T* operator->()
  69. {
  70. return _ptr;
  71. }
  72. T* get()
  73. {
  74. return _ptr;
  75. }
  76. /*防止拷贝的两种方法*/
  77. //法一:C++11
  78. unique_ptr( const unique_ptr<T>& up) = delete;
  79. unique_ptr<T>& operator=( const unique_ptr<T>& up) = delete;
  80. //法二:C++98
  81. /*private:
  82. //1、只声明,不实现
  83. //2、声明成私有
  84. unique_ptr(unique_ptr<T>& up);//拷贝构造
  85. unique_ptr& operator=(unique_ptr<T>& up);//拷贝赋值*/
  86. private:
  87. T* _ptr;
  88. };
  89. }

此时unique_ptr智能指针就不再害怕不是new出来的资源了。下面我们就来看看shared_ptr中的定制删除器。


shared_ptr的定制删除器

shared_ptr中的定制删除器不同于unique_ptr中的定制删除器,shared_ptr是在构造函数中支持的,而unique_ptr是在模板参数中支持的:

参数说明:

  • p:需要让智能指针管理的资源。
  • del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。 

示例如下:


   
  1. //针对new[]的释放
  2. template< class T>
  3. struct DeleteArray
  4. {
  5. void operator()(T* ptr)
  6. {
  7. cout << "delete[]" << ptr << endl;
  8. delete[] ptr;
  9. }
  10. };
  11. //针对malloc的释放
  12. template< class T>
  13. struct Free
  14. {
  15. void operator()(T* ptr)
  16. {
  17. cout << "free" << ptr << endl;
  18. free(ptr);
  19. }
  20. };
  21. //针对fopen的释放
  22. struct Fclose
  23. {
  24. void operator()(FILE* ptr)
  25. {
  26. cout << "fclose" << ptr << endl;
  27. fclose(ptr);
  28. }
  29. };
  30. int main()
  31. {
  32. //针对new的释放
  33. cpp::shared_ptr<Date> sp1(new Date); //默认new的释放
  34. //针对new[]的释放
  35. std::shared_ptr<Date> sp2(new Date[10], DeleteArray<Date>()); //传仿函数的匿名对象释放
  36. std::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr; }); //传lambda表达式释放
  37. //针对malloc的释放
  38. std::shared_ptr<Date> sp4((Date*)malloc(sizeof(Date) * 10), Free<Date>()); //传仿函数的匿名对象释放
  39. //针对fopen的释放
  40. std::shared_ptr<FILE> sp6((FILE*)fopen("Test.cpp", "r"), Fclose()); //传仿函数的匿名对象释放
  41. std::shared_ptr<FILE> sp5((FILE*)fopen("Test.cpp", "r"), [](FILE* ptr) {
  42. cout << "fclose: " << ptr << endl;
  43. fclose(ptr); });传lambda表达式释放
  44. return 0;
  45. }

注意:这里传对象作为定制删除器除了可以传仿函数,也可以传我们之前学过的lambda表达式,如上述代码所示。


5、C++11和boost中智能指针的关系

  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中的实现的。

转载:https://blog.csdn.net/bit_zyx/article/details/127696990
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场