C++11的智能指针
智能指针
总体介绍
智能指针是一个类似指针的类,提供了内存管理的功能,当指针不再被使用时,它指向的内存会自动被释放,这就比原生指针要好,原生指针有可能会因为忘记释放所申请的空间,而造成内存泄漏,而用智能指针就没这个顾虑。C++11支持shared_ptr, weak_ptr, unique_ptr,auto_ptr(被弃用)。这些智能指针位于<memory>
中
- auto_ptr采取所有权模式,可以被拷贝(构造or赋值)时,原auto_ptr指为nullptr,即auto_ptr被其他auto_ptr剥夺(转移),所以很容易引起内存泄露(粗心的程序员可能仍然会解引用原auto_ptr)
- unique_ptr是独占式拥有,解决了auto_ptr被剥夺的问题,unique_ptr禁止了拷贝(构造or赋值),保证同一时间内只有一个智能指针可以指向该对象,如果真的需要转移,可以使用借助move实现移动构造,原unique_ptr置为nullptr(但粗心的程序员可能仍然会解引用原来的unique_ptr)
- shared_ptr是共享,允许拷贝(构造or赋值),允许多个智能指针可以指向相同对象,每当,每次有一个shared_ptr关联到某个对象上时(拷贝构造or拷贝赋值),计数值就加上1;相反,每次有一个shared_ptr析构时,相应的计数值就减去1。当计数值减为0的时候,就执行对象的析构函数,此时该对象才真正被析构!如果用了移动(构造or赋值),那么原shared_ptr为空,并且指向对象的引用计数不会改变(相当于-1+1=0)
- weak_ptr是一种弱引用,指向shared_ptr(强引用)所管理的对象,可从一个shared_ptr或另一个weak_ptr来构造,它的构造和析构不会引起引用计数的增加或减少。weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象。weak_ptr提供了expired()与lock()成员函数,前者用于判断weak_ptr指向的对象是否已被销毁,后者返回其所指对象的shared_ptr智能指针(对象销毁时返回”空”shared_ptr)
话说智能指针发展之路(为了取消歧义,把复制都改为了拷贝,并且确定了是拷贝构造还是拷贝赋值)
1 | { |
shared_ptr与new
- 接受指针参数的智能指针构造函数是explicit的,因为不能将内置指针隐式转换成智能指针,必须使用直接初始化形式
- reset方法以改变shared_ptr
1 | shared_ptr<int> p1 = new int(1024); // 错误,必须使用直接初始化 |
- 不要混合使用智能指针和普通指针
- 若发生异常,普通指针不会自动销毁对象,但智能指针可以
- 如果用智能指针管理的资源不是new分配的内存,记得传递一个删除器
unique_ptr
- 一般使用release切断unique_ptr与它原来所管理的对象间的联系
- unique_ptr在函数返回时会执行一种特殊的拷贝
1 | u.release() // 放弃对指针的控制权,返回指针,并将u置空 |
最佳实践
这个对象在对象或方法内部使用时优先使用unique_ptr
这个对象需要被多个 Class 同时使用的时候优先使用shared_ptr
当出现循环引用的时候,用weak_ptr代替一个类中对其他类的shared_ptr引用
错误用法
使用智能指针托管的对象,尽量不要再使用原生指针(容易造成二次释放)
不要把一个原生指针交给多个智能指针管理(会导致多次销毁)
尽量不要使用 get()获取原生指针
不要将 this 指针直接托管智能指针(造成二次释放)
智能指针只能管理堆对象,不能管理栈上对象(造成二次释放)
make_shared的优缺点
1 | auto p = new widget(); |
缺点:
- 构造函数是保护或私有时,无法使用make_shared
- 对象的内存可能无法及时回收,用shared_ptr时,当强引用为0自动回收,用make_shared时,当强引用与弱引用都为0才自动回收
智能指针也会发生内存泄漏吗;如果是,有什么手段避免
两个shared_ptr相互引用时会发生循环引用(“你中有我,我中有你”),使引用计数失效,从而导致内存泄露
weak_ptr弱指针可以解决这个问题,weak_ptr的构造和析构不会影响引用计数,它指向shared_ptr所管理的对象,也可以检测到所管理的对象是否已经被释放,从而避免非法访问。
weak_ptr也可以调用lock()函数,如果管理对象没有被释放,则提升为shared_ptr,如果管理对象已经释放,调用lock()函数也不会有异常
shared_ptr循环引用导致内存泄漏
考虑下面的例子,A类与B类都有一个指向对方的shared_ptr成员,创建a_obj与b_obj,首先把if语句块注释掉,先测试它们不循环引用的情况
1 |
|
没有循环引用,通过下面的输出,可以看到a_obj与b_obj都已经正确地调用了析构函数。
1 | sa use count:1 |
为了探究use count的返回情况,修改if语句:
1 | if(sa && sb) |
输出如下,证实了局部引用在退出作用域时取消引用,use count会减1。
1 | sa use count:1 |
接下来修改if语句,探究a_obj与b_obj的shared_ptr互相引用对方时的场景
1 | if(sa && sb) |
输出如下,结束if语句块时,use count没有-1,证明了引用计数失效;程序结束时也没有kill B、kill A的输出,说明a_obj与b_obj没有正确析构
1 | sa use count:1 |
总结:循环引用导致引用计数失效,最后导致无法正确析构,造成内存泄漏
weak_ptr解决循环引用
只需要把class A与class B中的成员类型改为weak_ptr<B>
与weak_ptr<A>
即可,输出如下,引用计数正常工作,两个对象也正确析构了
1 | sa use count:1 |
手动实现引用计数型智能指针
这里简单地把引用计数设为int*,其实也可以构造一个类来代替int,在类中会有一个int成员变量
1 |
|