C++ — 智能指针的简单实现以及循环引用问题

电玩女神 2022-06-17 02:43 336阅读 0赞

智能指针

____________________________________________________

今天我们来看一个高大上的东西,它叫智能指针。 哇这个名字听起来都智能的不得了,其实等你了解它 你一定会有一 点失望的。。。。

因为它说白了 就是个管理资源的。智能指针的原理就是管理资源的RALL机 制,我们先来简单了解一下

RALL机制: RALL机制便是通过利用对象的自动销毁,使得资源也 具有了生命周期,有了自动销毁(自动回收)的功能。 RAII全称为

Resource Acquisition Is Initialization,它是在一些面向对象语言中的一种惯用 法。RAII 源于C++,在 Java,C#,D,Ada

,Vala和Rust中也有应用。 资源分配 即初始化,定义一个类来封装资源的分配和释放,在构造函数完 成资源的 分配和初始化,在析

构 函数完成 资源的清理,可以保证资源的正确初始化和释 放。RAII要求,资源的有效期与 持有资源的对象的 生命期严格绑定,即由

对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在 这种 要求下,只要对象能正确地析构,就不会出现资

源泄露问题。 RALL在这里就是简单提一下而已,现在我们来看我们 今天的主角智能指针。

智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类。它的诞生理由就是,为粗心和懒的 人 设计的, 但是这个设计一定

不是反人类 的,因为无论你有多厉害只要你是人你总会有犯错误的时候,所 以智能 指针可以很好地 帮助我们, 程序员每次 new 出来 的内

存 都要手动 delete 。程序 员忘记 delete ,流 程太复 杂, 最终导致没有 delete ,

异常导致程序过早退出,没有执行 delete 的情况并不罕见。其实智能 指针只是怕你 忘了 delete,而专门设置出来的 一 个对象。有没有

感觉它顿时不够 智能呢,但是你绝对不能否 认它的实用性和 重要性。 现在我们来看看智能指针的使用吧:

对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通 过析 构函 数释放有它管理的

堆内存。所有智 能指针都重载了“ operator-> ”操作符,直接返回对象的引用,用 以操作 对象。访 问智能指针原来的方法则使用“ . ”

操作符。 先抛开智能指针的几个 版本不说,我们先来讲一下它里面的 * 和 -> 是 如 何进行运算符重载的。 下面是我定义的一个类,他

只 是为了实现原生指针的 * 和 -> 功能:

  1. struct AA
  2. {
  3. int a = 10;
  4. int b = 20;
  5. };
  6. template<class T>
  7. class A
  8. {
  9. public:
  10. A(T* ptr)
  11. :_ptr(ptr)
  12. {}
  13. T* operator->()
  14. {
  15. return _ptr;
  16. }
  17. T& operator*()
  18. {
  19. return *_ptr;
  20. }
  21. A(A<T>& ap)
  22. {}
  23. A<T>& operator=(A<T>& ap)
  24. {}
  25. ~A()
  26. {delete _ptr;}
  27. protected:
  28. T* _ptr;
  29. };
  30. int main()
  31. {
  32. A<int>ap1(new int);
  33. *ap1 = 10;
  34. A<AA>ap2(new AA);
  35. cout << *ap1 << endl;
  36. cout << (ap2->a)<<" "<<(ap2->b) << endl;
  37. return 0;
  38. }

请忽略这个粗糙的A类和AA结构体,我们的目的只是实现原生函数的功能,那么我的功能实现了吗?

20170414200811731

这里结果没有一点问题,那么我们现在的注意点就应该放在这里是如何实现的:

Center

Center 1

智能指针的三大版本的实现==>

好了前面那些磨人的小妖精终于清理完了,现在我们真真正正的进入主题,智能指针的发展史以及它的常见的三个版本。

1.管理权转移 2.简单粗暴的防拷贝 3.引用计数版本

注意这里我只是实现简单的思想,可能写的不是很好,望大家指出帮助我改正错误。

管理权转移==>

这个智能指针是1998应用到VS上的,现在我们来实现第一个,何为管理权转移呢?

Center 2

现在我列出该思想的实现代码:

  1. template<class T>
  2. class AutoPtr
  3. {
  4. public:
  5. AutoPtr(T* ptr)
  6. :_ptr(ptr)
  7. {}
  8. T* operator->()
  9. {
  10. return _ptr;
  11. }
  12. T& operator*()
  13. {
  14. return *_ptr;
  15. }
  16. AutoPtr(AutoPtr<T>& ap)
  17. {
  18. this->_ptr = ap._ptr;
  19. ap._ptr = NULL;
  20. }
  21. AutoPtr<T>& operator=(AutoPtr<T>& ap)
  22. {
  23. if (this != &ap)
  24. {
  25. delete this->_ptr;
  26. this->_ptr = ap._ptr;
  27. ap._ptr = NULL;
  28. }
  29. return *this;
  30. }
  31. ~AutoPtr()
  32. {
  33. cout << "智能指针爸爸已经释放过空间了" << endl;
  34. delete _ptr;
  35. }
  36. protected:
  37. T* _ptr;
  38. };
  39. int main()
  40. {
  41. AutoPtr<int>ap1(new int);
  42. *ap1 = 10;
  43. AutoPtr<int>ap2(ap1);
  44. AutoPtr<int>ap3(ap2);
  45. *ap3 = 20;
  46. ap2 = ap3;
  47. cout << *ap2 <<endl;
  48. return 0;
  49. }

现在我们先看看它使用普通操作时的结果如何:

Center 3

现在的结果真的太符合我们的预料了,我们要的就是这样的结果,当你还沉浸自己成功的喜悦的时候 ,这里虽然 成功 实现了自动释放空

间的功能还有指 针的功能,但是看看下面这种情况: 我们把main函数内修改成这个样子:

int main()
{
AutoPtrap1(new int);
*ap1 = 10;
AutoPtrap2(ap1);
cout << *ap1 << endl;
return 0;
}

Center 4

然后结果。。调试到这一步程序崩溃了,罪魁祸首就是AutoPtrap2(ap1),这里原因就是ap2完全 的夺取了 ap1的 管理权。然后

导致ap1无家可归, 访问它的时候程序就会崩溃。如果在这里调用ap2 = ap1 程序一样会崩溃 原因还是ap1 被彻彻底底的夺走一切,

所 以这种编程思想及其不符合C++思想, 所以它的 设计思想就是有一定的缺 陷。 所以一般不推荐使用Autoptr智能指针。 使用了也绝

对不能使用”=”和拷贝构造。 历史在发展,所以我们见到接 下来这种想法:

简单粗暴法(防拷贝)==>

scoped智能指针 属于 boost 库,定义在 namespace boost 中,包含头文件#include 便可以 使用。

scoped智能指 针 跟 AutoPtr智能指针 一样,可以方便的管理单个堆内存对象,特别的是, scoped智能指针 独 享所有权,避免

了 AutoPtr智能指针 恼人的几个问 题,它直接就告诉用户我不提供”=”和拷贝 构造这两个功能,你别用, 用了我也让你编不过去。

来看它的实现:

  1. template<class T>
  2. class ScopedPtr
  3. {
  4. public:
  5. ScopedPtr()
  6. {}
  7. AutoPtr(T* ptr)
  8. :_ptr(ptr)
  9. {}
  10. T* operator->()
  11. {
  12. return _ptr;
  13. }
  14. T& operator*()
  15. {
  16. return *_ptr;
  17. }
  18. ~AutoPtr()
  19. {
  20. cout << "智能指针爸爸已经释放过空间了" << endl;
  21. delete _ptr;
  22. }
  23. protected:
  24. ScopedPtr(ScopedPtr<T>& s);
  25. ScopedPtr<T> operator=(ScopedPtr<T>& s);
  26. protected:
  27. T* _ptr;
  28. };

它的意思就是,我根本不会提供拷贝构造 和 “=”的功能,他强任他强,我就是这样。他确实解决上 一个智能指针 的问 题,他直接让用

户不能使用这个 功能,这个思想确实有点反人类。。 由于 scoped智能指针 独享所有权,当我们真真需要 复制智能指针时,需求便满足

不 了了,如此我们再 引入一 个智能 指针,专门用于处理复制,参数传递的情况。 这便是如下的shared 智能指针。

引用计数版本==>

接下来我们看最后一种,也就是我们现在经常用到的shared智能指针,等到智能指针发展到这一步也就很 成熟了 它已经 几乎完美的解

决所有功能,因为 它使用了引用计数版本当指向该片资源的*_num变成0的时候,释放该资源.

  1. template<class T>
  2. class shared
  3. {
  4. public:
  5. shared(T* ptr)
  6. :_ptr(ptr)
  7. , _num(new int(1))
  8. {
  9. }
  10. shared(const shared<T>& ap)
  11. :_ptr(ap._ptr)
  12. , _num(ap._num)
  13. {
  14. ++(*_num);
  15. }
  16. shared<T>& operator=(const shared<T>& ap)
  17. {
  18. if (_ptr != ap._ptr)
  19. {
  20. Release();
  21. _ptr = ap._ptr;
  22. _num = ap._num;
  23. ++(*_num);
  24. }
  25. return *this;
  26. }
  27. T* operator->()
  28. {
  29. return _ptr;
  30. }
  31. T& operator*()
  32. {
  33. return *_ptr;
  34. }
  35. void Release()
  36. {
  37. if (0 == (--*_num))
  38. {
  39. cout << "智能指针爸爸帮你释放空间了" << endl;
  40. delete _ptr;
  41. delete _num;
  42. _ptr = NULL;
  43. _num = NULL;
  44. }
  45. }
  46. ~shared()
  47. {
  48. Release();
  49. }
  50. protected:
  51. T* _ptr;
  52. int* _num;
  53. };
  54. int main()
  55. {
  56. shared<int>ap1(new int);
  57. *ap1 = 2;
  58. shared<int>ap2(ap1);
  59. cout << *ap2 << endl;
  60. shared<int>ap3(new int);
  61. ap3 = ap1;
  62. }

上面就是我实现的简易的shared智能指针,现在我们调用这个智能指针,我们来看看结果:

SouthEast

我们发现它完美的解决了一切功能,这个指针真的算是很完美的思想,不过你再完美也会有瑕疵,要不然也 不会 有 boost::weak_ptr

的存在, boost::weak_ptr的存在 就是为 boost::shared_ptr解决一点点瑕疵的。这个 问题藏 得极深 一 般 不会遇到的,但是当你真的

遇 到的时候,我相信你会 绞尽 脑汁的找BUG,还是很难找的。 话不多说,现在我们来看下面这个例子:

  1. struct ListNode
  2. {
  3. int _data;
  4. shared_ptr<ListNode> _prev;
  5. shared_ptr<ListNode> _next;
  6. ListNode(int x)
  7. :_data(x)
  8. , _prev(NULL)
  9. ,_next(NULL)
  10. {}
  11. ~ListNode()
  12. {
  13. cout << "~ListNode" << endl;
  14. }
  15. };
  16. int main()
  17. {
  18. shared_ptr<ListNode> cur(new ListNode(1));
  19. shared_ptr<ListNode> next(new ListNode(2));
  20. cur->_next = next;
  21. next->_prev = cur;
  22. cout << "cur" << " " << cur.use_count() << endl;
  23. cout << "next" << " " << next.use_count() << endl;
  24. return 0;
  25. }

现在我们验证shared智能指针的缺陷,就不用我实现的那个了,那个好多功能我都没实现,我们用专家 写 的 shared_ptr智 能指针,

构造两个双向链表里 面的结点,这里这个双向链表可能有一点简陋,但是我们只 是需要 它的prev和next指针就够了。 现在我们运

行代码看看会发生什么情况:

20170417163506782

现在cur和next指针所管理的结点现在都有两个指针指针管理,然后在这里会发生这样一件事:

20170417163706291

循环引用一般都会发生在这种”你中有我,我中有你”的情况里面,这里导致的问题就是内存泄漏,这段空间 一直都没有释 放,现在很

明显引用计数在这 里就不是很合适了,但是shared_ptr除了这里不够完善,其他的 地方都是非常有用的东西, 所以编写者在这里补

充一个week_ptr,接下来我们看最后一 个智能指针week_ptr。

week_ptr==>

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针, 因 为它不具 有普通指针

的行为,没有重载 operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者 那样 观测资源的使用情 况. 通俗一点讲就是,

首 先 weak_ptr 是专门为 shared_ptr 而 准备的。现在我们并不能根据 内部的 引用计数。 weak_ptr 是 boost::shared_ptr 的观察

者 对象,观察 者意味着 weak_p tr 只对 shared_ptr 进 行引用 而 不改变其引用计数,当被观 察的 shar ed_ptr 失效后,相应的

weak_ptr 也相应失效,然后它就什么都 不管光是个删 , 也就是这里的cur和next在析 构的时候 , 不用引用计数减一 , 直接删

除 结点就好。 这样也就间接地解决了循环引用的问题,当然week_ptr指针的功 能不是只有这一个。但是现在 我们只要 知道它可以解

决循环引用就好。

现在总结一下:

1、在可以使用 boost 库的场合下,拒绝使用 std::auto_ptr,因为其不仅不符合 C++ 编程思想。

2、在确定对象无需共享的情况下,使用 boost::scoped_ptr。

3、在对象需要共享的情况下,使用 boost::shared_ptr。

4、在需要访问 boost::shared_ptr 对象,而又不想改变其引用计数的情况下(循环引用)使用boost::weak_ptr。

5、最后一点,在你的代码中,尽量不要出现 delete 关键字,因为我们有智能指针。

发表评论

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

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

相关阅读

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

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

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

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

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

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