C++智能指针之shared_ptr详解
共享指针的初始化方式
1.裸指针直接初始化,但不能通过隐式转换来构造
2.允许移动构造,也允许拷贝构造
3.通过make_shared构造
例:
#include <iostream> #include <memory> class Frame {}; int main() { std::shared_ptr<Frame> f(new Frame()); // 裸指针直接初始化 std::shared_ptr<Frame> f1 = new Frame(); // Error,explicit禁止隐式初始化 std::shared_ptr<Frame> f2(f); // 拷贝构造函数 std::shared_ptr<Frame> f3 = f; // 拷贝构造函数 f2 = f; // copy赋值运算符重载 std::cout << f3.use_count() << " " << f3.unique() << std::endl; std::shared_ptr<Frame> f4(std::move(new Frame())); // 移动构造函数 std::shared_ptr<Frame> f5 = std::move(new Frame()); // Error,explicit禁止隐式初始化 std::shared_ptr<Frame> f6(std::move(f4)); // 移动构造函数 std::shared_ptr<Frame> f7 = std::move(f6); // 移动构造函数 std::cout << f7.use_count() << " " << f7.unique() << std::endl; std::shared_ptr<Frame[]> f8(new Frame[10]()); // Error,管理动态数组时,需要指定删除器 std::shared_ptr<Frame> f9(new Frame[10](), std::default_delete<Frame[]>()); auto f10 = std::make_shared<Frame>(); // std::make_shared来创建 return 0; }
注意:
1.尽量避免将一个裸指针传递给std::shared_ptr的构造函数,常用的替代手法是使用std::make_shared。如果必须将一个裸指针传递给shared_ptr的构造函数,就直接传递new运算符的结果,而非传递一个裸指针变量。
2.不要将this指针返回给shared_ptr。当希望将this指针托管给shared_ptr时,类需要继承自std::enable_shared_from_this,并且从shared_from_this()中获得shared_ptr指针。
3.不要使用相同的原始指针作为实参来创建多个shared_ptr对象,具体原因见下面讲的shared_ptr内存模型。可以使用拷贝构造或者直接使用重载运算符=进行操作
例:
#include <iostream> #include <memory> class Frame {}; int main() { Frame* f1 = new Frame(); std::shared_ptr<Frame> f2(f1); std::shared_ptr<Frame> f3(f1); // Error std::shared_ptr<Frame> f4(f2); auto f5 = f2; return 0; }
常用成员函数
s.get():返回shared_ptr中保存的裸指针;
s.reset(…):重置shared_ptr;
- reset( )不带参数时,若智能指针s是唯一指向该对象的指针,则释放,并置空。若智能指针P不是唯一指向该对象的指针,则引用计数减少1,同时将P置空。
- reset( )带参数时,若智能指针s是唯一指向对象的指针,则释放并指向新的对象。若P不是唯一的指针,则只减少引用计数,并指向新的对象。如:
auto s = make_shared<int>(100); s.reset(new int (200));
s.use_count()
:返回shared_ptr的强引用计数;
s.unique()
:若use_count()为1,返回true,否则返回false。
具体实例:
auto pointer = std::make_shared<int>(10); auto pointer2 = pointer; // 引用计数+1 auto pointer3 = pointer; // 引用计数+1 int *p = pointer.get(); // 这样不会增加引用计数 std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3 std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3 std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3 pointer2.reset(); std::cout << "reset pointer2:" << std::endl; std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2 std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0, pointer2 已 reset std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2 pointer3.reset(); std::cout << "reset pointer3:" << std::endl; std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1 std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0 std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 0, pointer3 已 reset
shared_ptr内存模型
由图可以看出,shared_ptr包含了一个指向对象的指针和一个指向控制块的指针。每一个由shared_ptr管理的对象都有一个控制块,它除了包含强引用计数、弱引用计数之外,还包含了自定义删除器的副本和分配器的副本以及其他附加数据。
控制块的创建规则
- std::make_shared总是创建一个控制块;
- 从具备所有权的指针出发构造一个std::shared_ptr时,会创建一个控制块(如std::unique_ptr转为shared_ptr时会创建控制块,因为unique_ptr本身不使用控制块,同时unique_ptr置空);
- 当std::shared_ptr构造函数使用裸指针作为实参时,会创建一个控制块。这意味从同一个裸指针出发来构造不止一个std::shared_ptr时会创建多重的控制块,也意味着对象会被析构多次。如果想从一个己经拥有控制块的对象出发创建一个std::shared_ptr,可以传递一个shared_ptr或weak_ptr而非裸指针作为构造函数的实参,或者直接使用重载运算符=,这样则不会创建新的控制块。
因此,更好的解决方式是尽量避免使用裸指针作为共享指针的实参,而是使用make_shared,此外,make_shared相比直接new还具有以下好处
make_shared的优缺点
优点
- 避免代码冗余:创建智能指针时,被创建对象的类型只需写1次,而用new创建智能指针时,需要写2次;
- 异常安全:make系列函数可编写异常安全代码,改进了new的异常安全性;
- 提升性能:编译器有机会利用更简洁的数据结构产生更小更快的代码。使用make_shared时会一次性进行内存分配,该内存单块(single chunck)既保存了T对象又保存与其相关联的控制块。而直接使用new表达式,除了为T分配一次内存,还要为与其关联的控制块再进行一次内存分配。
make_shared与new方式内存分布对比图:
缺点
- 所有的make系列函数都不允许自定义删除器;
- make系列函数创建对象时,不能接受{}初始化列表(这是因为完美转发的转发函数是个模板函数,它利用模板类型进行推导。因此无法将{}推导为initializer_list)。换言之,make系列只能将圆括号内的形参完美转发;
- **自定义内存管理的类(如重载了operator new和operator delete),不建议使用make_shared来创建。**因为:重载operator new和operator delete时,往往用来分配和释放该类精确尺寸(sizeof(T))的内存块;而make_shared创建的shared_ptr,是一个自定义了分配器(std::allocate_shared)和删除器的智能指针,由allocate_shared分配的内存大小也不等于上述的尺寸,而是在此基础上加上控制块的大小;
- 对象的内存可能无法及时回收。因为:make_shared只分配一次内存,减少了内存分配的开销,使得控制块和托管对象在同一内存块上分配。而控制块是由shared_ptr和weak_ptr共享的,因此两者共同管理着这个内存块(托管对象+控制块)。当强引用计数为0时,托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个weak_ptr离开作用域时,弱引用也减为0才会释放这块内存块。原本强引用减为0时就可以释放的内存, 现在变为了强引用和弱引用都减为0时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说, 是一个需要注意的问题。
引用计数
- shared_ptr中的引用计数直接关系到何时是否进行对象的析构,因此它的变动尤其重要。
- shared_ptr的**构造函数会使该引用计数递增,而析构函数会使该计数递减。**但移动构造表示从一个己有的shared_ptr移动构造到一个新的shared_ptr。这意味着一旦新的shared_ptr产生后,原有的shared_ptr会被置空,其结果是引用计数没有变化;
- 拷贝赋值操作同时执行两种操作(如sp1和sp2是指向不同对象的shared_ptr,则执行sp1=sp2时,将修改sp1使得其指向sp2所指的对象。而最初sp1所指向的对象的引用计数递减,同时sp2所指向的对象引用计数递增);
- reset函数,如果不带参数时,则引用计数减1。如果带参数时,如sp.reset( p )则sp原来指向的对象引用计数减1,同时sp指向新的对象( p );
- 如果实施一次递减后最后的引用计数变成0,即不再有shared_ptr指向该对象,则会被shared_ptr析构掉;
- 引用计数的递增和递减是原子操作,即允许不同线程并发改变引用计数。
比较运算符
所有比较运算符都会调用共享指针内部封装的原始指针的比较运算符;支持==、!=、<、<=、>、>=;同类型的共享指针才能使用比较运算符
shared_ptr<int> sp_n1 = make_shared<int>(1); shared_ptr<int> sp_n2 = make_shared<int>(2); shared_ptr<int> sp_nu; shared_ptr<double> sp_d1 = make_shared<double>(1); bool bN1LtN2 = sp_n1 < sp_n2; //true bool bN1GtNu = sp_n1 > sp_nu; //true bool bNuEqNu = sp_nu == sp_nu; //true bool bN2GtD1 = sp_d1 < sp_n2; //编译错误