C++-智能指针——简单实现分析

亦凉 2022-06-11 08:28 305阅读 0赞

一:为什么要有智能指针

在我们动态开辟内存时,每次new完就一定会有配套的delete来完成释放操作。可是这时候问题就来了,有时候程序未必会执行到我们释放的那一步,说不定在中间部分就进行了异常的跳转又或者是在判断是否释放的条件上出错,都会导致未按照我们预想的那样完成空间的释放,从而造成内存泄漏。

这时候就有人利用RAII思想实现了智能指针,所以说白了所谓智能指针就是智能/自动化的管理指针所指向的动态资源的释放。而所谓的智能主要就是利用类的默认成员函数中的析构函数的特性。


二:智能指针的发展历史

这里写图片描述


二:各类智能指针的核心思想以及简单实现


<1>auto_ptr

核心思想实现如下:

  1. //自动指针,管理权限转移
  2. template <class T>
  3. class AutoPtr
  4. {
  5. public:
  6. AutoPtr(T* ptr)
  7. :_ptr(ptr)
  8. {}
  9. ~AutoPtr()
  10. {
  11. cout << "~AutoPtr()" << endl;
  12. delete _ptr;
  13. }
  14. AutoPtr(AutoPtr<T>& ap)
  15. :_ptr(ap._ptr)
  16. {
  17. ap._ptr = NULL;
  18. }
  19. AutoPtr<T>& operator=(AutoPtr<T>& ap)
  20. {
  21. if (_ptr != ap._ptr)
  22. {
  23. if (_ptr)
  24. {
  25. delete _ptr;
  26. }
  27. _ptr = ap._ptr;
  28. ap._ptr = NULL;
  29. }
  30. return *this;
  31. }
  32. T& operator*()
  33. {
  34. return *_ptr;
  35. }
  36. T* operator->()
  37. {
  38. return _ptr;
  39. }
  40. private:
  41. T* _ptr;
  42. };

当我们进行简单的操作并不会出现问题:

这里写图片描述

但是!由于auo_ptr核心思想是管理权限转移,当有新的指针进行拷贝构造或者赋值时,就会剥夺旧指针的管理权,将旧指针置空,所以当我们再次访问旧指针时,就会报错,然而这并不是我们所期待的。

这里写图片描述


<2>scoped_ptr

在auto_ptr的基础上,我们就会发现只有进行拷贝构造或者赋值运算时,再次对旧指针操作就会出错,所以scoped_ptr索性直接就不让用户在使用的时候进行这两个操作,也就避免了之后错误的发生。至于实现:要想根本不让拷贝和赋值,①可以只声明不定义,这样只要用户调就会报错;②将声明放入私有中,防止定义。当完成后,用户再次调用时就会报:无法访问 private 成员(在“ScopedPtr”类中声明)的错误。

核心思想实现如下:

  1. //守卫指针。防止拷贝
  2. template <class T>
  3. class ScopedPtr
  4. {
  5. public:
  6. ScopedPtr(T* ptr)
  7. :_ptr(ptr)
  8. {}
  9. ~ScopedPtr()
  10. {
  11. if (_ptr)
  12. {
  13. delete _ptr;
  14. cout << " ~ScopedPtr()" << endl;
  15. }
  16. }
  17. T& operator*()
  18. {
  19. return *_ptr;
  20. }
  21. T* operator->()
  22. {
  23. return _ptr;
  24. }
  25. private:
  26. ScopedPtr(const ScopedPtr<T>& ptr);
  27. ScopedPtr<T>& operator=(ScopedPtr<T>& ptr);
  28. protected:
  29. T* _ptr;
  30. };

我们不使用拷贝功能的时候还好,但是我们一旦真的需要使用带拷贝功能的智能指针时,这时候就需要引入shared_ptr了。


<3>shaered_ptr

在我们真正想完成指针拷贝或者赋值时,真正的想法是让两个指针同时指向一片空间,而不是像auto_ptr那样的管理权转移,这时候就有一种解决办法是加入引用计数

核心思想实现如下:

  1. //引入引用计数
  2. template <class T>
  3. class SharedPtr
  4. {
  5. public:
  6. SharedPtr(T* sp)
  7. :_ptr(sp)
  8. ,_count(new int(1)) //在初始化时置1
  9. {}
  10. ~SharedPtr()
  11. {
  12. cout << "~SharedPtr()" << endl;
  13. Release();
  14. }
  15. SharedPtr(SharedPtr<T>& sp)
  16. :_ptr(sp._ptr)
  17. ,_count(sp._count)
  18. {
  19. (*_count)++;
  20. }
  21. SharedPtr<T>& operator= (SharedPtr<T>& sp)
  22. {
  23. if (_ptr != sp._ptr)
  24. {
  25. Release();
  26. _ptr = sp._ptr;
  27. _count = sp._count;
  28. (*_count)++;
  29. }
  30. return *this;
  31. }
  32. T& operator*()
  33. {
  34. return *_ptr;
  35. }
  36. T* operator->()
  37. {
  38. return _ptr;
  39. }
  40. void Release()
  41. {
  42. if (--(*_count) == 0)
  43. {
  44. delete _ptr;
  45. delete _count;
  46. _ptr = NULL;
  47. _count = NULL;
  48. }
  49. }
  50. int GetCount()//获取count
  51. {
  52. return *_count;
  53. }
  54. T* GetPtr() const//获取指针
  55. {
  56. return _ptr;
  57. }
  58. protected:
  59. T* _ptr;
  60. int* _count;
  61. };

简单的测试后,发现确实引入引用计数后,解决了拷贝的问题。但是有一个缺陷,就是循环引用问题处理。

这里写图片描述


<4>weak_ptr

首先我们先来分析下循环引用的问题,既然weak_ptr是用来解决shared_ptr的缺陷,那就先研究下缺陷到底是什么。

这里写图片描述

上图就是简单的循环引用问题:假设所有内部的指针都是shared_ptr,在最初完成构造时,left和right指针所指向的节点计数都为1,但是当left中的next指针指向right节点时,right所指向的节点中的count++为2;同样,right的prev指针指向left所指向的节点时,left所指向的节点中count++也为2。

这时候问题就出现了!由于我们使用的指针均为shared_ptr,就会出现如下的循环逻辑:

①左侧节点要想释放,必须内部count为0,但是由于右侧节点中的_prev指针也指向该空间,导致内部计数为2,所以只有当右侧节点中的_prev指针被释放了,才能够使指向左侧节点的指针只有left一个,–count为0才能完成节点的释放;

②然而右侧节点中的_prev指针要想释放,就得先等右侧节点释放,但是右侧节点还有左侧节点中的_next指针也指向该空间,只有等左侧节点中的_next释放了,才能够完成右侧节点的释放,然后再次回到第一步逻辑。

这时候就引入了weak_ptr专门负责解决。我们只需要把节点的_next和_prev指针都改为weak_ptr,因为weak_ptr不会增加shared_ptr所指向空间的引用计数,这样在释放的时候就不会有循环引用的问题。


核心实现如下:

  1. template <class T>
  2. class WeakPtr
  3. {
  4. public:
  5. WeakPtr()
  6. :_ptr(NULL)
  7. {}
  8. WeakPtr(const SharedPtr<T>& sp)
  9. :_ptr(sp.GetPtr())
  10. {}
  11. ~WeakPtr()
  12. {
  13. if (_ptr)
  14. {
  15. cout << "~WeakPtr" << endl;
  16. delete _ptr;
  17. _ptr = NULL;
  18. }
  19. }
  20. T& operator*()
  21. {
  22. return *_ptr;
  23. }
  24. T* operator->()
  25. {
  26. return _ptr;
  27. }
  28. private:
  29. T* _ptr;
  30. };

注:在我们平常使用时,如果是支持C++11的编译器,就可以直接包含memory库(如下),然后使用对应的函数进行操作。前提是一定要熟悉各种指针的特性。

这里写图片描述


小结:

1:能用智能指针来管理空间时,就尽量不出现malloc(new)或free(delete);

2:在可以使用其他指针的前提下,拒绝使用std::auto_ptr;

3:在确定对象不会出现共享的情况下,使用boost::scoped_ptr(数组为boost::scopted-array)或c++新加的unique_ptr;

4:在确定对象出现共享的情况下,使用shared_ptr;

5:共享指针若会出现循环引用时,及时使用weak_ptr。

发表评论

表情:
评论列表 (有 0 条评论,305人围观)

还没有评论,来说两句吧...

相关阅读

    相关 C++智能指针简单剖析

    智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。 1. 智能指针背后的设

    相关 基于C++实现一个简单智能指针

    在C、C++类的语言当中对指针的使用是十分常见和重要的,但是使用指针也很容易导致内存泄漏、不安全的情况发生,本文就针对这种情况来实现一个简单的智能指针类,通过这个类实现对指针操

    相关 C++-智能指针——简单实现分析

    > 一:为什么要有智能指针 在我们动态开辟内存时,每次new完就一定会有配套的delete来完成释放操作。可是这时候问题就来了,有时候程序未必会执行到我们释放的那一步,