shared_ptr、weak_ptr
参考https://blog.csdn.net/ArtAndLife/article/details/120793343
shared_ptr的使用
C++智能指针的头文件:
#include <memory>
shared_ptr:智能指针从本质上来说是一个模板类,用类实现对指针对象的管理。
shared_ptr初始化的两种方式:
一、用new创建的裸指针初始化shared_ptr
尽量避免“先new、后用裸指针初始化shared_ptr” 的方式。否则,当有两个或多个shared_ptr同时管理一个指针时,多个shared_ptr之间无法共享彼此的引用计数。
int *ptr = new int(42);
shared_ptr<int> sp1(ptr);
shared_ptr<int> sp2(ptr);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
//sp1记录的引用计数是1,sp2记录的引用计数也是1,
//此时有两个智能指针sp1和sp2同时管理ptr,相当于有两个独立的控制块
return 0;
//此时退出作用域,sp1、sp2会分别调用delete去释放基础对象*ptr,
//重复释放,导致程序段错误
二、使用make_shared直接创建shared_ptr
make_shared 是 函数模板,不是类模板,make_shared函数模板的返回值类型是 shared_ptr。但make_shared只能调用传入类型的公有构造函数。
优点:
- 效率更高:
假设原始对象类型为 widget,shared_ptr的“控制块”中需要维护的关于“引用计数”的信息包括:强引用:用来计数当前有多少存活的shared_ptr正持有该对象,共享的对象会在最后一个强引用离开的时候释放;弱引用:用来记录当前有多少个正在观察该对象的weak_ptr,当最后一个弱引用离开的时候,共享的内部信息控制块会被释放。
1、如果通过原始的new表达式分配对象,然后传递给shared_ptr(即使用shared_ptr内部的构造函数),则**“控制块内存”与“基础对象内存”是分离开的**,需要两个分配内存的动作(new创建基础对象内存、shared_ptr的构造函数创建),所以控制块与基础对象的内存是分离的(可能造成内存碎片)。控制块的内存是在shared_ptr的构造函数中分配的。
2、make_shared 的方式,则只需要一次分配内存,控制块与基础对象的内存是连续的 - 异常安全:
可能会出现异常的情况:因为C++是不保证参数求值顺序,以及内部表达式的求值顺序,导致new分配的裸指针没有立即初始化shared_ptr,从而导致new分配的对象内存可能因为中间抛出的某个异常无法释放!!!
//函数F的定义:
void F(shared_ptr<Lhs>& lhs, shared_ptr<Rhs>& rhs) { ... }
//调用F函数:
F(shared_ptr<Lhs>(new Lhs("foo")), shared_ptr<Rhs>(new Rhs("bar")));
//C++是不保证 参数求值顺序,以及 内部表达式的求值顺序 的,所以可能的执行顺序如下:
new Lhs("foo")
new Rhs("bar")
shared_ptr<Lhs>
shared_ptr<Rhs>
如果程序在第2步时抛出一个异常(比如out of memory等,Rhs的构造函数异常的),那么在第1步中new分配的Lhs对象内存将无法释放,导致内存泄漏。这个问题的核心在于 shared_ptr 没有立即获得new分配出来的裸指针,shared_ptr与new结合使用时是要分成两步。
修复这个问题的方式有两种:
(1)不要将new操作放到函数形参初始化中,这样将无法保证求值顺序:
//解决方法是先保证两个new分配内存都没有错误,并在new之后立即初始化shared_ptr:
auto lhs = shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);
(2)更推荐的方法,是使用make_shared,一步到位 :
F(make_shared<Lhs>("foo"), make_shared<Rhs>("bar"))
缺点:
- 构造函数是保护或私有时,无法使用make_shared;
- 对象内存可能无法及时回收:
智能指针的“控制块”中保存着两类关于“引用计数”的信息:强引用;(strong refs)弱引用。(weak refs)。“弱引用计数”用来保存当前正在指向此基础对象的weak_ptr指针的个数,weak_ptr会保持控制块的生命周期,因此有一种特殊情况是:强引用的引用计数已经降为0,没有shared_ptr再持有基础对象,然而由于仍有weak_ptr指向基础对象,弱引用的引用计数非0,原本因为强引用计数已经归0就可以释放的基础对象内存,现在变成了“强引用、弱引用都减为0时才能释放”,意外的延迟了内存释放的时间。这对于内存要求高的场景来说,是一个需要注意的问题。
解决的问题
- 忘记释放资源导致的内存泄漏;
通过new分配的堆内存没有使用delete进行释放。
- 多个指针指向同一资源时可能产生的悬垂指针;
普通指针出现悬垂的根本原因在于:当多个指针同时指向同一个内存资源时,如果通过其中的某一个指针delete释放了资源,其他指针无法感知到。
解决办法:引用计数,对“控制块”中“引用计数”的管理:
构造函数: 当创建类的新对象时,初始化指针,并将引用计数设置为 1; 拷贝构造函数: 当对象作为另一个对象的副本时(即发生“拷贝构造”时),拷贝构造函数拷贝副本指针,并对引用计数 加1; 拷贝赋值运算符:
当使用“拷贝赋值运算符”(=)时,处理复杂一点: a. 先使“左操作数”的指针的引用计数减1
(为何减一:因为该指针已经指向别的地方,则指向原基础对象的指针个数减1), 如果减1后引用计数降为0,则释放指针所指对象的内存资源;
b. 然后增加“右操作数”所指对象的引用计数(为何加一:因为此时左操作数转而指向此基础对象,则指向此基础对象的指针个数加1);
析构函数: 调用析构函数时,析构函数先使引用计数减1,如果减至0则delete释放对象。
shared_ptr的实现
在典型的实现中,shared_ptr 只保有两个指针:
get()所返回的指针;(基础对象的内存地址)
指向控制块的指针。(控制块对象的内存地址)
控制块是一个动态分配的对象,其中包含:
指向被管理对象的指针或被管理对象本身;(基础对象的内存地址)
删除器;(Deleter,类型擦除)
分配器;(Allocator,类型擦除)
占用被管理对象的shared_ptr的数量(strong refs强引用的引用计数);
涉及被管理对象的weak_ptr的数量(weak refs弱引用的引用计数) 。
shared_ptr的线程安全性
shared_ptr的引用计数本身是安全且无锁的;
std::atomic提供了一组原子操作,包括load、store、exchange等。这些原子操作可以保证对一个变量的操作是原子性的,即不会被其他线程打断。对于std::shared_ptr来说,它的引用计数就是一个std::atomic类型的对象。当有新的std::shared_ptr指向同一个对象时,引用计数加一;当某个std::shared_ptr被销毁或者不再指向该对象时,引用计数减一。这些操作都是通过原子操作来实现的,因此是线程安全的。
为了确保内存的可见性,std::shared_ptr还使用了内存屏障(memory barrier)。内存屏障是一种同步原语,用于防止编译器或处理器对指令进行重排序,从而保证程序的正确性。在std::shared_ptr中,内存屏障主要用于保证引用计数的更新操作在其他线程能够看到这个更新之前完成。
shared_ptr中封装的基础对象的读写不是线程安全。
shared_ptr所封装的基础对象的读写操作并不是线程安全的。既不是原子操作,也没有加锁。
shared_ptr::swap, reset操作
spr1.swap(spr2);
每一个shared_ptr指针内部都保有两个指针,分别指向基础对象的地址和控制块对象的地址,swap将spr1和spr2的这两个指针互换了。表现:spr2管理着原先spr1管理的基础对象,spr1管理着原先spr2管理的基础对象,并没有发生对象的析构;spr2与spr1指向的控制块互换了,是指针保存的地址互换了,并没有发生内存拷贝。
spr1.reset();
指向基础对象的指针变成nullptr,指向的控制块的指针将控制块的引用计数减1,然后将其指向一个独立的控制块(引用计数=0)
shared_from_this
多个shared_ptr管理同一指针时的重复释放问题
使用shared_ptr的原则:
- 当我们使用智能指针管理资源时,必须统一使用智能指针,而不能在某些地方使用智能指针,某些地方使用raw
pointer,否则不能保持智能指针管理这个类对象的语义,从而产生各种错误。 - 给shared_ptr管理的资源必须在分配时立即交给shared_ptr,即:shared_ptr sp(new T());,而不是先new出ptr,再在后面的某个地方将ptr赋给shared_ptr。
shared_from_this的使用场景
返回当前对象的this指针,但直接传递this指针(相当于裸指针)到类外,有可能会被多个shared_ptr所管理,造成与上面一样的二次释放的异常错误。
使用shared_ptr直接管理this指针导致“重复释放”的原因在于: 使用智能指针管理“类对象”的本质是管理类对象的 this 指针;
this指针与其他的普通裸指针并无区别,当多个shared_ptr同时管理同一个this指针时,相互之间无法感知。
C++11 引入 shared_from_this,使用方式如下:
继承 enable_shared_from_this 类; 调用 shared_from_this()
成员函数先将this指针封装进一个shared_ptr,再将shared_ptr返回到类外供其他人使用。
weak_ptr:
weak_ptr的特性:
- weak_ptr 只能从shared_ptr构建;
- weak_ptr并不影响动态对象的生命周期,即其存在与否并不影响对象的引用计数(强引用的引用计数);
- weak_ptr 没有重载 operator-> 和operator* 操作符,因此不可以直接通过 weak_ptr使用对象(必须通过weak_ptr获取到shared_ptr后才能访问基础对象,,通过lock()成员函数返回shared_ptr指针);
- weak_ptr 提供了 expired() 和lock() 成员函数,分别用于判断基础对象是否已被销毁、返回指向基础对象的shared_ptr指针。
weak_ptr的使用场景:
- 当你想使用对象,但是并不管理对象,并且在需要的时候可以返回对象的shared_ptr时,则使用
- 解决shared_ptr的“循环引用”问题。
循环引用:比如有两个类A和B,在A里面使用std::shared_ptr引用了B,在B里面使用std::shared_ptr引用了A,形成循环引用,std::shared_ptr的析构是引用计数为0的时候才会析构。那么在析构B的时候,会先将自己的成员(引用A的std::shared_ptr成员)析构掉,故要先去析构A,但是在析构A的时候,又要将A的成员(引用B的std::shared_ptr成员)析构掉
这里就可以用weak_ptr替代std::shared_ptr,既不会影响析构,也可以在需要时通过weak_ptr的lock()函数获取指向基础对象的shared_ptr指针。
unique_ptr:
unique_ptr的设计:
- 禁止拷贝构造函数、拷贝赋值运算符,即设置为=delete;
- 实现了移动构造函数和移动赋值运算符。
- unique_ptr必须直接初始化,且不能通过隐式转换来构造,因为unique_ptr的构造函数被声明为explicit。
unique_ptr 的常用操作:
u.get(); //返回unique_ptr中保存的裸指针
u.reset(); //重置unique_ptr
u.release(); //放弃指针的控制权,返回裸指针,并将unique_ptr自身置为空。
u.swap(); //交换两个unique_ptr所指向的对象