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
- 使用auto_ptr的误区:
两auto_ptr不要引用同一个对象(t*),这样如果一个auto_ptr释放了资源,另一个或多个就无法获得引用对象的信息。
auto_ptr不可以保存array,因为在释放的时候是使用delete,这样就会引起未知的问题。
auto_ptr设计成在异常发生的时候避免内存泄漏,不要乱用。
不要在容器中使用auto_ptr,因为无法满足容器的一些要求,比如在copy construct和 “=”操作时,会转移所有权。
auto_ptr<string> p1(new string("I reigned lonely as a cloud."));
auto_ptr<string> p2;
p2 = p1; // auto_ptr不会报错.
/*
* 此时不会报错,p2剥夺了p1的所有权,但是当程序运⾏时访问p1将会报错。
* 所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
*/
5. shared_ptr
5.1 实现原理
使用计数机制来表明资源被几个指针共享。
- 构造函数中计数初始化为1;
- 拷⻉构造函数中计数值加1;
- 赋值运算符中,左边的对象引⽤计数减1,右边的对象引⽤计数加1;
- 析构函数中引⽤计数减1;
- 在赋值运算符和析构函数中,如果减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 相互循环引用,造成的内存泄漏
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include"boost/shared_ptr.hpp"
struct Node {
Node(int value): _value(value) { cout << "Node()" << endl; }
~Node() { cout << "~Node()" << endl; }
shared_ptr<Node> _prev;
shared_ptr<Node> _next;
//使用weak_ptr 可以解决stared_ptr的相互引用问题。
//weak_ptr<Node> _prev;
//weak_ptr<Node> _next;
int _value;
};
void Test2() {
shared_ptr<Node> sp1(new Node(1));
shared_ptr<Node> sp2(new Node(2));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
int main() {
Test2();
return 0;
}
由于先构造的后释放,后构造的先释放可知,先释放的是sp2,那么因为它的引用计数为2,减去1之后就成为了1,不能释放空间,因为还有其他的对象在管理这块空间。但是sp2这个变量已经被销毁,因为它是栈上的变量,但是sp2管理的堆上的空间并没有释放。
接下来释放sp1,同样,先检查引用计数,由于sp1的引用计数也是2,所以减1后成为1,也不会释放sp1管理的动态空间。
通俗点讲:就是sp2要释放,那么必须等p1释放了,而sp1要释放,必须等sp2释放,所以,最终,它们两个都没有释放空间。
6. unique_ptr
采用所有权模式
unique_ptr<string> p1(new string("I reigned lonely as a cloud."));
unique_ptr<string> p2;
p2 = p1; // 此时会报错.
/*
* 编译器认为p2=p1⾮法,避免了p1不再指向有效数据的问题。
* 因此,unique_ptr⽐auto_ptr更安全
*/
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
/*
* 当程序试图将⼀个 unique_ptr 赋值给另⼀个时,
* 如果源 unique_ptr 是个临时右值,编译器允许这么做;
* 如果源 unique_ptr 将存在⼀段时间,编译器将禁⽌这么做,
*/
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。⽽#2不会留下悬挂的unique_ptr,因为它调⽤unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况⽽已的⾏为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
NOTE:如果确实想执⾏类似与#1的操作,要安全的重⽤这种指针,可给它赋新值。C++有⼀个标准库函数std::move(),让你能够将⼀个unique_ptr赋给另⼀个。例如:
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
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相互引用的死锁问题?
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
shared_ptr<B> pb_;
~A() { cout << "A delete" << endl; }
};
class B {
public:
shared_ptr<A> pa_;
~B() { cout << "B delete" << endl; }
};
int main()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl;
cout << pa.use_count() << endl;
return 0;
}
// 输出结果:2 2
可以看到main函数中pa,pb之间互相引⽤,两个资源的引⽤计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引⽤计数会减⼀,但是两者引⽤计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调⽤)。
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
weak_ptr<B> pb_;
~A() { cout << "A delete" << endl; }
};
class B {
public:
shared_ptr<A> pa_;
~B() { cout << "B delete" << endl; }
};
int main()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl;
cout << pa.use_count() << endl;
// pb->pa_->Printf(); not allow
shared_ptr<A> pbpa = pb->pa_.lock();
pbpa->Printf();
return 0;
}
// 输出结果:
1
2
B delete
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();