当前位置:   article > 正文

C++|智能指针

C++|智能指针

目录

引入

一、智能指针的使用及原理

1.1RAII

1.2智能指针原理

1.3智能指针发展

1.3.1std::auto_ptr

1.3.2std::unique_ptr

1.3.3std::shared_ptr 

二、循环引用问题及解决方法

2.1循环引用

2.2解决方法 

三、删除器

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


引入

回顾上一篇章,学习了异常机制,但面临了一种情况还没有解决,就是异常带来的内存泄漏,如下:

  1. #include <iostream>
  2. using namespace std;
  3. double Division(int a, int b)
  4. {
  5. // 当b == 0时抛出异常
  6. if (b == 0)
  7. throw "Division by zero condition!";//抛出的异常是字符串
  8. else
  9. return ((double)a / (double)b);
  10. }
  11. void Func()
  12. {
  13. int* p = new int[1];
  14. int len, time;
  15. cin >> len >> time;
  16. cout << Division(len, time) << endl;
  17. delete[] p;
  18. }
  19. int main() {
  20. try
  21. {
  22. Func();
  23. }
  24. catch (const char* errmsg)
  25. {
  26. cout << "Caught in main: " << errmsg << endl;
  27. }
  28. return 0;
  29. }

调用Division函数时,若抛出异常,最终是被main函数中的catch所捕获,直接进入到main函数中的catch中了,但是,delete[] p不会被执行了 ,导致的问题就是内存泄漏。那么可以通过智能指针来解决这个问题

一、智能指针的使用及原理

1.1RAII

RAII是一种利用对象生命周期来控制程序资源的简单技术。在对象构造时获取对象资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候由编译器顺带释放资源。实际上把控资源就是管理对象。

这种做法体现的优势是:

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

RAII是一种设计思想,而智能指针正是采用了这种思想。

RAII思想设计: 

  1. #include <iostream>
  2. using namespace std;
  3. // 使用RAII思想设计的SmartPtr类
  4. template<class T>
  5. class SmartPtr {
  6. public:
  7. SmartPtr(T* ptr = nullptr)
  8. :_ptr(ptr)
  9. {}
  10. ~SmartPtr()
  11. {
  12. if (_ptr)
  13. delete _ptr;
  14. }
  15. private:
  16. T* _ptr;
  17. };
  18. double Division(int a, int b)
  19. {
  20. // 当b == 0时抛出异常
  21. if (b == 0)
  22. throw "Division by zero condition!";//抛出的异常是字符串
  23. else
  24. return ((double)a / (double)b);
  25. }
  26. void Func()
  27. {
  28. int* p = new int[1];
  29. int len, time;
  30. cin >> len >> time;
  31. cout << Division(len, time) << endl;
  32. SmartPtr<int> sp(p);//当Division抛异常时,会被main函数中的catch捕获,那么程序会直接跳到main函数所在的catch
  33. //对此了,Func函数的栈帧会自动销毁,对于内置类型会自动释放,对于自定义类型而言会去调用它的析构函数
  34. //所以sp会去调用析构函数,同时释放了p的资源
  35. }
  36. int main() {
  37. try
  38. {
  39. Func();
  40. }
  41. catch (const char* errmsg)
  42. {
  43. cout << "Caught in main: " << errmsg << endl;
  44. }
  45. return 0;
  46. }

输出结果:

1.2智能指针原理

上述的SmartPtr虽然是通过RAII思想设计的类,但还不能真正称作为智能指针,因为它还不有指针的行为。指针可以解引用,也可以通过->去访问所指空间的内容,因此还需要重载*和->。

  1. #include <iostream>
  2. using namespace std;
  3. template<class T>
  4. class SmartPtr {
  5. public:
  6. SmartPtr(T* ptr = nullptr)
  7. : _ptr(ptr)
  8. {}
  9. ~SmartPtr()
  10. {
  11. if (_ptr)
  12. delete _ptr;
  13. }
  14. T& operator*()
  15. {
  16. return *_ptr;
  17. }
  18. T* operator->()
  19. {
  20. return _ptr;
  21. }
  22. private:
  23. T* _ptr;
  24. };
  25. struct Date
  26. {
  27. int _year;
  28. int _month;
  29. int _day;
  30. };
  31. int main()
  32. {
  33. SmartPtr<int> sp1(new int);
  34. *sp1 = 10;
  35. cout << *sp1 << endl;
  36. SmartPtr<Date> sparray(new Date);
  37. sparray->_year = 2024;
  38. sparray->_month = 7;
  39. sparray->_day = 7;
  40. cout << (*sparray)._year << ":" << (*sparray)._month << ":" << (*sparray)._day << endl;
  41. return 0;
  42. }

输出结果:

总结一下,智能指针是通过RAII思想设计出的类,它重载operator*和operator->,具有像指针一样的行为。 

1.3智能指针发展

对于上述代码,却有一个弊端,如下:

  1. int main()
  2. {
  3. SmartPtr<int> sp1(new int);
  4. SmartPtr<int> sp2(sp1);
  5. return 0;
  6. }

 带来的问题是,sp1拷贝给sp2是一个浅拷贝,所以sp2._ptr和sp1._ptr指向同一块空间,在析构时候,sp2._ptr所指向空间先释放,随后释放sp1._ptr所指向空间,但是该空间已被释放,所以会报错。那么为了解决这个问题,在历史上,智能指针有了一定的发展,来看。

1.3.1std::auto_ptr

C++98版本库中提供了auto_ptr的智能指针。他上述问题的实现原理是:通过管理权转移思想。

  1. // C++98 管理权转移 auto_ptr
  2. #include <iostream>
  3. using namespace std;
  4. namespace bit
  5. {
  6. template<class T>
  7. class auto_ptr
  8. {
  9. public:
  10. auto_ptr(T* ptr)
  11. :_ptr(ptr)
  12. {}
  13. //ap2(ap1)
  14. auto_ptr(auto_ptr<T>& sp)
  15. :_ptr(sp._ptr)
  16. {
  17. // 管理权转移
  18. sp._ptr = nullptr;//相当于ap1._ptr不再管理资源了,被置空了
  19. }
  20. //ap2= ap1
  21. auto_ptr<T>& operator=(auto_ptr<T>& ap)
  22. {
  23. // 检测是否为自己给自己赋值
  24. if (this != &ap)
  25. {
  26. // 释放当前对象中资源
  27. if (_ptr)
  28. delete _ptr;
  29. // 转移ap中资源到当前对象中
  30. _ptr = ap._ptr;
  31. ap._ptr = NULL;
  32. }
  33. return *this;
  34. }
  35. ~auto_ptr()
  36. {
  37. if (_ptr)
  38. {
  39. cout << "delete:" << _ptr << endl;
  40. delete _ptr;
  41. }
  42. }
  43. // 像指针一样使用
  44. T& operator*()
  45. {
  46. return *_ptr;
  47. }
  48. T* operator->()
  49. {
  50. return _ptr;
  51. }
  52. private:
  53. T* _ptr;
  54. };
  55. }
  56. int main()
  57. {
  58. std::auto_ptr<int> sp1(new int);
  59. std::auto_ptr<int> sp2(sp1); // 管理权转移,sp1不再管理资源了,在析构的时候,确实没啥问题
  60. //但带来了另一个问题,sp1所管理资源,被置空了,即sp1._ptr1 == nullptr,但对于外人来说并不知道
  61. //若有人使用已被释放的资源,如下:
  62. *sp2 = 10;
  63. cout << *sp2 << endl;
  64. cout << *sp1 << endl;//对空指针解引用,报错
  65. return 0;
  66. }

auto_ptr指针虽然解决了析构带来的问题,但是带来了新的问题,采用管理权转移思想,将资源由最后一个拷贝对象管理,而被拷贝对象都被置空了,导致有人使用被置空的拷贝对象时,会出现对空指针解引用,报错。所以该智能指针也可以算是一个"失败的man",很多公司也明确要求不能使用它。

1.3.2std::unique_ptr

随着C++的发展,C++11提供了更靠谱的智能指针unique_ptr,其实现原理:简单粗暴的防拷贝。

  1. #include <iostream>
  2. using namespace std;
  3. namespace bit
  4. {
  5. template<class T>
  6. class unique_ptr
  7. {
  8. public:
  9. unique_ptr(T* ptr)
  10. :_ptr(ptr)
  11. {}
  12. ~unique_ptr()
  13. {
  14. if (_ptr)
  15. {
  16. cout << "delete:" << _ptr << endl;
  17. delete _ptr;
  18. }
  19. }
  20. // 像指针一样使用
  21. T& operator*()
  22. {
  23. return *_ptr;
  24. }
  25. T* operator->()
  26. {
  27. return _ptr;
  28. }
  29. //只声明不实现,但是不能防止有人在类外实现,所以此方法不行
  30. //unique_ptr(unique_ptr<T>& sp);
  31. //c++11仿拷贝
  32. /*unique_ptr(const unique_ptr<T>& sp) = delete;
  33. unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;*/
  34. private:
  35. T* _ptr;
  36. //将拷贝私有化可以防拷贝,但这是c++98用法
  37. //unique_ptr(unique_ptr<T>& sp);
  38. };
  39. }
  40. //类外实现拷贝
  41. //template<class T>
  42. //bit::unique_ptr<T>::unique_ptr(bit:: unique_ptr<T>& sp)
  43. //{
  44. // //...
  45. //}
  46. int main()
  47. {
  48. bit::unique_ptr<int> sp1(new int);
  49. //bit::unique_ptr<int> sp2(sp1);//拷贝构造被删除了,编译器也不会默认生成了
  50. return 0;
  51. }

 由于前面出现的问题都是因为拷贝带来的,那么unique_ptr了就直接把拷贝给禁了,不让你因为各种操作而导致额外的问题,这确实解决了。但是不能使用拷贝了,这也不是办法呀

1.3.3std::shared_ptr 

随着前面智能指针问题的暴露,C++11不断发展,最终也完善了该问题,提出了更靠谱且支持拷贝的shared_ptr。其原理:是通过引用计数方式来实现多个shared_ptr对象之间共享资源。

其满足以下规则:

1.每个shared_ptr对象,其都包含着一个指针,该指针所指向空间内容用来计数有多少个指针指向该空间,即有多少个对象管理同一份资源,当多个对象管理同一份资源,那么他们的指针就会指向同一份空间,空间内容记录着这些数量,每增加一个对象管理相同资源,则计数也会加1

2.当对象销毁时即调用析构函数,就说明该对象不再管理这份资源,其对应的指针不再指向计数的空间,引用计数就会减1

3.如果引用计数是0,就说明没有对象管理该资源了,则必须释放该资源,相反地,如果不是0,说明还有对象管理资源,则就不能释放该资源,否则,对于其他对象中的指针就成野指针了。

  1. #include <iostream>
  2. using namespace std;
  3. namespace bit
  4. {
  5. template<class T>
  6. class shared_ptr
  7. {
  8. public:
  9. shared_ptr(T* ptr = nullptr)
  10. :_ptr(ptr)
  11. , _pcount(new int(1))
  12. {}
  13. shared_ptr(const shared_ptr<T>& sp)
  14. {
  15. if (this != &sp)
  16. {
  17. _ptr = sp._ptr;
  18. _pcount = sp._pcount;
  19. (*_pcount)++;
  20. }
  21. }
  22. shared_ptr<T>& operator=(const shared_ptr<T>& sp)
  23. {
  24. if (this != &sp)
  25. {
  26. if (--(*sp._pcount) == 0)
  27. {
  28. delete _ptr;
  29. delete _pcount;
  30. }
  31. _ptr = sp._ptr;
  32. _pcount = sp._pcount;
  33. (*_pcount)++;
  34. }
  35. return *this;
  36. }
  37. ~shared_ptr()
  38. {
  39. if (--(*_pcount) == 0)
  40. {
  41. delete _ptr;
  42. delete _pcount;
  43. _ptr = nullptr;
  44. _pcount = nullptr;
  45. }
  46. }
  47. T& operator*()
  48. {
  49. return *_ptr;
  50. }
  51. T* operator->()
  52. {
  53. return _ptr;
  54. }
  55. private:
  56. T* _ptr;
  57. int* _pcount;
  58. };
  59. }

调试结果: 

shared_ptr解决了拷贝带来的问题,不幸的是,又出来了新的问题,那就是在线程安全和循环引用问题,对于线程问题,先放放,那我们要讲的是循环引用问题。

二、循环引用问题及解决方法

2.1循环引用

根据上述shared_ptr的代码,在外头定义一个类,进行以下操作:

  1. struct ListNode
  2. {
  3. bit::shared_ptr<ListNode> _next;
  4. bit::shared_ptr<ListNode> _prev;
  5. ~ListNode()
  6. {
  7. cout << "~ListNode()" << endl;
  8. }
  9. };
  10. int main()
  11. {
  12. bit::shared_ptr<ListNode> node1(new ListNode);
  13. bit::shared_ptr<ListNode> node2(new ListNode);
  14. node1->_next = node2;
  15. node2->_prev = node1;
  16. return 0;
  17. }

输出结果:

没有任何结果,而当屏蔽node1->_next = node2;或者node2->_prev = node1;中的任何一句,

其输出结果:

这是为何?如图:

两边的节点各自受到_next,_prev牵连,造成了一个循环,且引用计数一直维持在1,并没有减到0,只要屏蔽其中一条语句就不会受到限制,正常析构。虽然这个智能指针是模拟的,就算是库里面的也有一样的问题。

2.2解决方法 

那么为了解决该问题,专门引入了一个智能指针来处理--weak_ptr

  1. #include <iostream>
  2. using namespace std;
  3. namespace bit
  4. {
  5. template<class T>
  6. class shared_ptr
  7. {
  8. public:
  9. shared_ptr(T* ptr = nullptr)
  10. :_ptr(ptr)
  11. , _pcount(new int(1))
  12. {}
  13. shared_ptr(const shared_ptr<T>& sp)
  14. {
  15. if (this != &sp)
  16. {
  17. _ptr = sp._ptr;
  18. _pcount = sp._pcount;
  19. ++(*_pcount);
  20. }
  21. }
  22. shared_ptr<T>& operator=(const shared_ptr<T>& sp)
  23. {
  24. if (this != &sp)
  25. {
  26. if (--(*_pcount) == 0)
  27. {
  28. delete _ptr;
  29. delete _pcount;
  30. }
  31. _ptr = sp._ptr;
  32. _pcount = sp._pcount;
  33. ++(*_pcount);
  34. }
  35. return *this;
  36. }
  37. ~shared_ptr()
  38. {
  39. if (--(*_pcount) == 0)
  40. {
  41. delete _ptr;
  42. delete _pcount;
  43. _ptr = nullptr;
  44. _pcount = nullptr;
  45. }
  46. }
  47. T& operator*()
  48. {
  49. return *_ptr;
  50. }
  51. T* operator->()
  52. {
  53. return _ptr;
  54. }
  55. T* get() const
  56. {
  57. return _ptr;
  58. }
  59. private:
  60. T* _ptr;
  61. int* _pcount;
  62. };
  63. // 简化版本的weak_ptr实现
  64. template<class T>
  65. class weak_ptr
  66. {
  67. public:
  68. weak_ptr()
  69. :_ptr(nullptr)
  70. {}
  71. weak_ptr(const shared_ptr<T>& sp)
  72. :_ptr(sp.get())
  73. {}
  74. weak_ptr<T>& operator=(const shared_ptr<T>& sp)
  75. {
  76. _ptr = sp.get();
  77. return *this;
  78. }
  79. T& operator*()
  80. {
  81. return *_ptr;
  82. }
  83. T* operator->()
  84. {
  85. return _ptr;
  86. }
  87. private:
  88. T* _ptr;
  89. };
  90. }
  91. struct ListNode
  92. {
  93. bit::weak_ptr<ListNode> _next;
  94. bit::weak_ptr<ListNode> _prev;
  95. ~ListNode()
  96. {
  97. cout << "~ListNode()" << endl;
  98. }
  99. };
  100. int main()
  101. {
  102. bit::shared_ptr<ListNode> node1(new ListNode);
  103. bit::shared_ptr<ListNode> node2(new ListNode);
  104. node1->_next = node2;
  105. node2->_prev = node1;
  106. return 0;
  107. }

 输出结果:

那么weak_ptr是怎样解决问题的了,其实很简单,weak_ptr不在对引用计数进行管理了,所以shared_ptr对象初始化时的引用计数都是为1,在进行析构的时候,引用计数都会减到0,然后释放对应的_ptr,_pcount。

三、删除器

对于智能指针的析构还有一个问题,就是对于不是通过new出来的对象或者说new出来的对象是带有[]的如何通过智能指针进行管理,总不能每一种情况来一份代码。那么shared_ptr设计了一个删除器来解决这个问题。

  1. // 仿函数的删除器
  2. #include <iostream>
  3. using namespace std;
  4. template<class T>
  5. struct FreeFunc {
  6. void operator()(T* ptr)
  7. {
  8. cout << "free:" << ptr << endl;
  9. free(ptr);
  10. }
  11. };
  12. template<class T>
  13. struct DeleteArrayFunc {
  14. void operator()(T* ptr)
  15. {
  16. cout << "delete[]" << ptr << endl;
  17. delete[] ptr;
  18. }
  19. };
  20. int main()
  21. {
  22. FreeFunc<int> freeFunc;
  23. shared_ptr<int> sp1((int*)malloc(4), freeFunc);
  24. DeleteArrayFunc<int> deleteArrayFunc;
  25. shared_ptr<int> sp2(new int[4], deleteArrayFunc);
  26. shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p)
  27. {fclose(p); });
  28. return 0;
  29. }

输出结果:

如上代码,删除器可以通过自己定义的方式来释放空间,这只是展示了仿函数删除器,仅仅是九牛一毛。

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

Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,是一个"准"标准库,由Boost社区组织开发、维护。Boost库可以与C++标准库完美共同工作,并且为其提供扩展功能。

1.C++98 中产生了第一个智能指针auto ptr.

不好的设计,对象悬空(不建议使用)
2.C++ boost给出了更实用的scoped ptr和shared ptr和weak ptr.

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

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号