1. 概述
shared_ptr智能指针,本质是“离开作用域会自动调整(减小)引用计数,如果引用计数为0,则会调用析构函数”。这样一来,就进化成类似于int、float等的一种会被自动释放的类型。
2. 初始化智能指针
初始化一个智能指针的方式比较多,可以构造一个空的智能指针,也可以通过调用new进行初始化,最推荐的还是通过make_shared<T>来构造。
如下是几种构造方式。
(1)构造空的智能指针
shared_ptr<T> ptr;
就相当于一个 NULL 指针
(2)调用new
从new操作符的返回值构造
shared_ptr<T> ptr(new T());
(3)拷贝构造
shared_ptr<T> ptr2(ptr1);
使用拷贝构造函数的方法,会让引用计数加 1。
shared_ptr 可以当作函数的参数传递,或者当作函数的返回值返回,这个时候其实也相当于使用拷贝构造函数。
(4)使用make_shared构造
比较推荐使用make_shared辅助创建
std::shared_ptr<T>foo = std::make_shared<T>(10);
此处仅以构造时形参为一个int为例,实际情况可变。
3. 具有继承关系的智能指针转换
当两个类具有继承关系时,我们需要使用dynamic_pointer_cast或static_pointer_cast进行一个转换。
在介绍两个API之前,我们先定义两种转换形式。假设我们有两个类,分别是Base类和Derived类,其中Derived类继承自Base类。
下行转换:Base类的指针指向Drived类;
下行转换:Drived类的指针指向Base类;
(1)dynamic_pointer_cast
当我们一般进行下行转换时,一般使用dynamic_pointer_cast。这样在不确定下行转换是否可行时,可以进行对象实际类型的检查,如果不能够转换,则返回NULL指针。
/** 假设B是A的子类 */shared_ptr<B> ptrb(new B());shared_ptr<A> ptra(dynamic_pointer_cast<A>(ptrb) ); ///< 从shared_ptr提供的类型转换(dynamic_pointer_cast)函数的返回值构造
(2)static_pointer_cast
既可以用在上行转换,又可以用在下行转换。
需要注意的是,用于下行转换时,并不会进行类型的检查,如果不能够转换,会发生未定义的行为。因此,使用static_pointer_cast的前提是,开发者需要确切的指导下行转换是都是什么样的类型,开发者需要了解能不能转换。
4. 智能指针赋值
如下程序所示,在赋值以后a原先所指的对象会被销毁,b所指的对象引用计数加1。
shared_ptr可以直接赋值,但是必须是赋给相同类型的shared_ptr对象,而不能是普通的C指针或new运算符的返回值。
当共享指针a被赋值成b的时候,如果a原来是NULL, 那么直接让a等于b并且让它们指向的东西的引用计数加1;
如果a原来也指向某些东西的时候,如果a被赋值成b, 那么原来a指向的东西的引用计数被减1, 而新指向的对象的引用计数加1。
/** shared_ptr 的“赋值” */shared_ptr<T> a(new T());shared_ptr<T> b(new T());a = b;
5. 智能指针重置
我们在使用智能指针过程中,偶尔会重置,将智能指针指向其他对象,这个时候可以使用reset成员。
/** 已定义的共享指针指向新的new对象: reset() */shared_ptr<T> ptr(new T());ptr.reset(new T());
如上操作,原来所指的对象会被销毁。
6. 常用函数
get(): 获取源类型指针;
use_count();获取引用计数;
reset():重置指针为NULL;
7. shared_ptr存在的问题
可以说shared_ptr是一种非常实用化的应用形式,但也存在一些问题。如下:
(1)内存无法及时释放
只有当循环计数为0时,才会释放内存;
(2)循环引用
当出现循环引用时,易出现内存泄漏,可利用weak_ptr解决;
8. 优缺点
讲完了如何使用,接下来我们讲一下优缺点。
(1)效率更高
shared_ptr 需要维护引用计数的信息,
强引用, 用来记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个强引用离开的时候销毁( 也可能释放).
弱引用, 用来记录当前有多少个正在观察该对象的 weak_ptrs. 当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话).
(2)分配方式灵活
如果你通过使用原始的 new 表达式分配对象, 然后传递给 shared_ptr (也就是使用 shared_ptr 的构造函数) 的话, shared_ptr 的实现没有办法选择, 而只能单独的分配控制块:
auto p = new widget();
shared_ptr sp1{ p }, sp2{ sp1 };
如果选择使用 make_shared 的话, 情况就会变成下面这样:
auto sp1 = make_shared(), sp2{ sp1 };
内存分配的动作, 可以一次性完成. 这减少了内存分配的次数, 而内存分配是代价很高的操作.
关于两种方式的性能测试可以看这里 Experimenting with C++ std::make_shared
(3)异常安全
看看下面的代码:
void Func(const std::shared_ptr<Lhs>& lhs, const std::shared_ptr<Rhs>& rhs){/* ... */}/** 调用函数,导入两个智能指针. */Func(std::shared_ptr<Lhs>(new Lhs("foo")), std::shared_ptr<Rhs>(new Rhs("bar")));
C++ 不能保证参数求值顺序, 以及内部表达式的求值顺序的, 所以可能的执行顺序如下:
new Lhs(“foo”))
new Rhs(“bar”))
std::shared_ptr
std::shared_ptr
此时如果在第 2 步的时候, 抛出了一个异常, 那么第一步申请的 Lhs 对象内存泄露了. 这个问题的核心在于, shared_ptr 没有立即获得裸指针.
我们可以用如下方式来修复这个问题.
auto lhs = std::make_shared<Lhs>("foo");auto rhs = std::make_shared<Rhs>("bar");Func(lhs, rhs);
缺点
(1)构造函数是保护或私有时,无法使用 make_shared
make_shared 虽好, 但也存在一些问题, 比如, 当我想要创建的对象没有公有的构造函数时, make_shared 就无法使用了, 当然我们可以使用一些小技巧来解决这个问题, 比如这里 How do I call ::std::make_shared on a class with only protected or private constructors?
(2)对象的内存可能无法及时回收
make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 若引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题. 关于这个问题可以看这里 make_shared, almost a silver bullet