auto_ptr(c++被摒弃)/shared_ptr/unique_ptr/weak_ptr


1. 智能指针设计思想:

作用:解决内存泄漏和空悬指针问题。
具体实现:智能指针是一个RAII模型,用于动态内存分配。将普通类型指针封装成(模板)类对象指针(模版类,便于适应不同类型的需求)。在离开作用域时调用析构函数,删除指针指向的内存空间。

2. 智能指针种类

STL提供了四种智能指针:auto_ptr, shared_ptr,unique_ptr,weak_ptr

  • auto_ptr:实现独占式拥有的概念,同一时间只能有一个智能指针可以指向该对象。(所有权模式)

被C++11摒弃,主要原因:具体证明?

  • 对象所有权的转移,⽐如在函数传参过程中,对象所有权不会返还,从⽽存在潜在的内存崩溃问题
  • 不能指向数组,也不能作为STL容器的成员
    • shared_ptr:实现共享式拥有的概念,即多个智能指针可以指向相同的对象。它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr, weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
    • unique_ptr(替代auto_ptr):实现独占式拥有的概念,同一时间只能有一个智能指针可以指向该对象。替代auto_ptr,比auto_ptr更安全。是一个独享所有权的智能指针,它提供了严格意义上的所有权。无法进行拷贝构造和拷贝赋值构造,但是可以进行移动构造和移动赋值。
    • weak_ptr:weak_ptr是用来解决shared_ptr相互引用时的死锁问题。如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

      3. 如何选择智能指针?

      ① 使用多个指向同一个对象的指针,使用shared_ptr。共享指针。
  • 两个对象包含都指向第三个对象的指针;
  • 有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
  • STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。

② 不需要多个指向同一个对象的指针,则可使用unique_ptr。唯一指针。

  • 如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。
  • 可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。

    4. auto_ptr

  1. 使用auto_ptr的误区:

两auto_ptr不要引用同一个对象(t*),这样如果一个auto_ptr释放了资源,另一个或多个就无法获得引用对象的信息。
auto_ptr不可以保存array,因为在释放的时候是使用delete,这样就会引起未知的问题。
auto_ptr设计成在异常发生的时候避免内存泄漏,不要乱用。
不要在容器中使用auto_ptr,因为无法满足容器的一些要求,比如在copy construct和 “=”操作时,会转移所有权。

  1. auto_ptr<string> p1(new string("I reigned lonely as a cloud."));
  2. auto_ptr<string> p2;
  3. p2 = p1; // auto_ptr不会报错.
  4. /*
  5. * 此时不会报错,p2剥夺了p1的所有权,但是当程序运⾏时访问p1将会报错。
  6. * 所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
  7. */

5. shared_ptr

5.1 实现原理

使用计数机制来表明资源被几个指针共享。

  1. 构造函数中计数初始化为1
  2. 拷⻉构造函数中计数值加1
  3. 赋值运算符中,左边的对象引⽤计数减1,右边的对象引⽤计数加1
  4. 析构函数中引⽤计数减1;
  5. 在赋值运算符和析构函数中,如果减1后为0,则调⽤ delete 释放对象。

5.2 shared_ptr的缘来

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使⽤引⽤计数的机制上提供了可以共享所有权的智能指针。
成员函数:
use_count 返回引⽤计数的个数
unique 返回是否是独占所有权( use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引⽤计数的减少
get(): 返回内部对象(指针), 由于已经重载了()⽅法, 因此和直接使⽤对象是⼀样的.如 shared_ptr sp(new int(1)); sp与sp.get()是等价的

5.2 shared_ptr 相互循环引用,造成的内存泄漏

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include<iostream>
  3. using namespace std;
  4. #include"boost/shared_ptr.hpp"
  5. struct Node {
  6. Node(int value): _value(value) { cout << "Node()" << endl; }
  7. ~Node() { cout << "~Node()" << endl; }
  8. shared_ptr<Node> _prev;
  9. shared_ptr<Node> _next;
  10. //使用weak_ptr 可以解决stared_ptr的相互引用问题。
  11. //weak_ptr<Node> _prev;
  12. //weak_ptr<Node> _next;
  13. int _value;
  14. };
  15. void Test2() {
  16. shared_ptr<Node> sp1(new Node(1));
  17. shared_ptr<Node> sp2(new Node(2));
  18. cout << sp1.use_count() << endl;
  19. cout << sp2.use_count() << endl;
  20. sp1->_next = sp2;
  21. sp2->_prev = sp1;
  22. cout << sp1.use_count() << endl;
  23. cout << sp2.use_count() << endl;
  24. }
  25. int main() {
  26. Test2();
  27. return 0;
  28. }

image.png

由于先构造的后释放,后构造的先释放可知,先释放的是sp2,那么因为它的引用计数为2,减去1之后就成为了1,不能释放空间,因为还有其他的对象在管理这块空间。但是sp2这个变量已经被销毁,因为它是栈上的变量,但是sp2管理的堆上的空间并没有释放。
接下来释放sp1,同样,先检查引用计数,由于sp1的引用计数也是2,所以减1后成为1,也不会释放sp1管理的动态空间。
通俗点讲:就是sp2要释放,那么必须等p1释放了,而sp1要释放,必须等sp2释放,所以,最终,它们两个都没有释放空间。

6. unique_ptr

采用所有权模式

  1. unique_ptr<string> p1(new string("I reigned lonely as a cloud."));
  2. unique_ptr<string> p2;
  3. p2 = p1; // 此时会报错.
  4. /*
  5. * 编译器认为p2=p1⾮法,避免了p1不再指向有效数据的问题。
  6. * 因此,unique_ptr⽐auto_ptr更安全
  7. */
  8. unique_ptr<string> pu1(new string ("hello world"));
  9. unique_ptr<string> pu2;
  10. pu2 = pu1; // #1 not allowed
  11. unique_ptr<string> pu3;
  12. pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
  13. /*
  14. * 当程序试图将⼀个 unique_ptr 赋值给另⼀个时,
  15. * 如果源 unique_ptr 是个临时右值,编译器允许这么做;
  16. * 如果源 unique_ptr 将存在⼀段时间,编译器将禁⽌这么做,
  17. */

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。⽽#2不会留下悬挂的unique_ptr,因为它调⽤unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况⽽已的⾏为表明,unique_ptr 优于允许两种赋值的auto_ptr
NOTE:如果确实想执⾏类似与#1的操作,要安全的重⽤这种指针,可给它赋新值。C++有⼀个标准库函数std::move(),让你能够将⼀个unique_ptr赋给另⼀个。例如:

  1. unique_ptr<string> ps1, ps2;
  2. ps1 = demo("hello");
  3. ps2 = move(ps1);
  4. ps1 = demo("alexia");
  5. cout << *ps2 << *ps1 << endl;

7. weak_ptr

weak_ptr 是⼀种不控制对象⽣命周期的智能指针, 它指向⼀个 shared_ptr 管理的对象. 进⾏该对象的内存管理的是那个强引⽤的 shared_ptr. weak_ptr只是提供了对管理对象的⼀个访问⼿段。 weak_ptr 设计的⽬的:是为配合shared_ptr ⽽引⼊的⼀种智能指针来协助 shared_ptr ⼯作, 它只可以从⼀个 shared_ptr 或另⼀个 weak_ptr 对象构造, 它的构造和析构不会引起引⽤记数的增加或减少。weak_ptr是⽤来解决shared_ptr相互引⽤时的死锁问题。它是对对象的⼀种弱引⽤,不会增加对象的引⽤计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调⽤lock函数来获得shared_ptr。

7.1 weak_ptr如何解决shared_ptr相互引用的死锁问题?

  1. #include <iostream>
  2. #include <memory>
  3. using namespace std;
  4. class B;
  5. class A {
  6. public:
  7. shared_ptr<B> pb_;
  8. ~A() { cout << "A delete" << endl; }
  9. };
  10. class B {
  11. public:
  12. shared_ptr<A> pa_;
  13. ~B() { cout << "B delete" << endl; }
  14. };
  15. int main()
  16. {
  17. shared_ptr<B> pb(new B());
  18. shared_ptr<A> pa(new A());
  19. pb->pa_ = pa;
  20. pa->pb_ = pb;
  21. cout << pb.use_count() << endl;
  22. cout << pa.use_count() << endl;
  23. return 0;
  24. }
  25. // 输出结果:2 2

可以看到main函数中pa,pb之间互相引⽤,两个资源的引⽤计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引⽤计数会减⼀,但是两者引⽤计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调⽤)。

  1. #include <iostream>
  2. #include <memory>
  3. using namespace std;
  4. class B;
  5. class A {
  6. public:
  7. weak_ptr<B> pb_;
  8. ~A() { cout << "A delete" << endl; }
  9. };
  10. class B {
  11. public:
  12. shared_ptr<A> pa_;
  13. ~B() { cout << "B delete" << endl; }
  14. };
  15. int main()
  16. {
  17. shared_ptr<B> pb(new B());
  18. shared_ptr<A> pa(new A());
  19. pb->pa_ = pa;
  20. pa->pb_ = pb;
  21. cout << pb.use_count() << endl;
  22. cout << pa.use_count() << endl;
  23. // pb->pa_->Printf(); not allow
  24. shared_ptr<A> pbpa = pb->pa_.lock();
  25. pbpa->Printf();
  26. return 0;
  27. }
  28. // 输出结果:
  29. 1
  30. 2
  31. B delete
  32. A delete

如果把其中⼀个(任何一个即可)改为weakptr就可以了,我们把类A⾥⾯的shared_ptr pb; 改为weakptr pb; 资源B的引⽤开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减⼀,同时pa析构时使A的计数减⼀,那么A的计数为0,A得到释放。
「注意」不能通过weak_ptr直接访问对象的⽅法,⽐如B对象中有⼀个⽅法print(),我们不能这样访问,pa->pb->print(); 英⽂pb是⼀个weakptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb.lock(); p->print();