裸指针存在的问题

  1. 裸指针在声明时并没有指出所指向的是单个对象还是数组
  2. 裸指针声明时没有提示在使用完指向的对象后是否需要析构它。我们无法从声明中看出指针是否拥有其指向的对象
  3. 即使需要析构,由于不清楚其指向单个对象还是数组,也不知道该如何析构它,该使用delete还是delete[]或是其它专门的析构函数中。
  4. 我们需要保证所有指针的析构执行并且只执行一次,因为不执行析构就会导致资源泄露,多次执行则会产生未定义行为。
  5. 我们无法检测出一个指针是否是空悬指针,即所指向的对象已被析构。
  6. 为了解决这一问题,因此C++11引入了智能指针,unique_ptr用以取代C++98中的auto_ptr,只有当使用C++98编译器时我们才使用auto_ptr,其余地方auto_ptr都可以用uniqu_ptr代替。

使用std::unique_ptr管理具备专属所有权的资源

  1. 当我们需要使用智能指针时,unique_ptr基本上是首选,因为它和裸指针有着相同的尺寸,并且对于大多数的操作都执行了相同的指令。
  2. 一个非空的unique_ptr总是拥有其所指向的对象和资源,移动一个unique_ptr会将所有权从源指针移至目标指针,同时unique_ptr不允许复制因为要是允许复制那么就会有两个unique_ptr指向同一对象,不符合unique_ptr的性质。
  3. 默认unique_ptr指向对象的析构是调用内部裸指针的delete操作完成的。
  4. unique_ptr的一个常见用法是在对象继承谱系中作为工厂函数的返回类别。
  5. 我们可以设置使用自定义析构器:析构资源时所调用的任意函数(或函数对象,包括那些由lambda表达式产生的)。所有自定义删除函数都接受一个指向欲析构对象的裸指针,然后采取必要措施析构该对象,使用lambda表达式创建一个删除函数很方便。
  6. 要使用自定义析构器时,其类别必须被指定为unique_ptr的第二个实参,也就是自定义删除函数的类别。
  7. 将一个裸指针赋给unique_ptr是编译不过的,因为这样会造成从裸指针到智能指针的隐式转换,这种隐式转换大有问题,因此需要reset来指定让unique_ptr获取new运算符产生的对象的所有权。
  8. 在使用默认析构器(delete函数)的前提下,unique_ptr与裸指针尺寸相同,但是使用自定义析构器时就不一样了,析构器是函数指针的话尺寸往往增加1到2个字长(word),如果是函数对象则尺寸变化取决于该函数对象中存储的状态,而无状态的函数对象(如无捕获的lambda表达式)不会浪费任何尺寸,因此使用lambda表达式实现自定义析构函数往往是个更好的选择。
  9. unqiue_ptr以两种形式提供,一种是单个对象(std::unqiue_ptr,一种是数组(std::unqiue_ptr<T[]>),但是使用容器字符串等几乎总是比裸数组更好,因此很少使用。
  10. std::unqiue_ptr还能高效的转换为std::shared_ptr。

使用std::shared_ptr管理具备共享所有权的资源

  1. 没有哪个特定的shared_ptr拥有其指向的对象,所有指向其的所有shared_ptr共同协作,当最后一个指向该对象的shared_ptr不再指向它时,该shared_ptr会析构其指向的对象。
  2. shared_ptr用访问某资源的引用计数,即指向该资源的shared_ptr数量来确定自己是否是最后一个指向该资源的shared_ptr。shared_ptr构造函数会让该计数递增,析构函数会让该计数递减。
  3. 复制赋值操作符sp1=sp2会递减sp1原指向对象的引用计数,递增sp2指向对象的引用计数。如果某个shared_ptr析构完发现递减完后引用计数变为0,shared_ptr会析构其指向的资源。
  4. 引用计数的存在将会带来一些性能影响:
    (1)shared_ptr的尺寸是裸指针的两倍,既包含裸指针也包含指向该资源的引用计数的裸指针
    (2)引用计数的内存必须动态分配,shared_ptr若是使用std::make_shared创建可避免动态分配的成本,但是有些场景无法使用make_ptr。但是无论使不使用make_ptr引用计数都会作为动态分配的数据来存储。
    (3)引用计数的递增和递减必须是原子操作,因为不同线程可能会发生并发的读写器,原子操作一般比非原子操作慢。
  5. 移动shared_ptr不会发生引用计数递增操作,而复制则需要,因此移动会比复制操作快,这一点对于构造和赋值操作都成立。
  6. shared_ptr默认析构器依然是delete,也支持自定义析构器,但是对于unique_ptr来说,析构器类型是其类型的一部分,当两个unique_ptr存储相同类型的裸指针但不同类型的析构器时他们不是一个类型,而对于shared_ptr来说析构器类型并不是其类型的一部分,当二者裸指针类型相同析构器类型不同时也可以认为是同一个类型。
  7. 并且与unique_ptr不同的是自定义析构器类型不会影响其尺寸大小。每一个由shared_ptr管理的对象都有一个控制块除了包含引用计数外还包含自定义析构器的一个复制,如果该自定义析构器被指定的话,如果指定了一个自定义内存分配器控制块也会包含一份它的复制,控制块还可能包含其他附加数据,比如弱引用计数等。
  8. 一个对象的控制块由创建首个指向该对象的shared_ptr的函数来确定,而控制块的创建遵循以下规则:
    (1)std::make_shared总是创建一个控制块,make_shared会生产出一个用以指向的对象,因此调用make_shared时不会有该对象的控制块存在
    (2)从具备专属所有权的指针(即auto_ptr或unique_ptr)出发构造一个shared_ptr指针时也会创建一个控制块
    (3)当shared_ptr构造函数使用裸指针作为实参调用时,它会创建一个控制块。如果使用的是shared_ptr或者weak_ptr作为实参,则不会创建新的控制块,因为它们可以依赖传入的智能指针以指向任意所需的控制块。
  9. 从一个裸指针出发构造不止一个shared_ptr的话,被指向的对象将有多重的控制块,也意味着多重的引用计数,也就是多次的析构,也就会导致未定义行为。
  10. 因此尽可能避免将裸指针传递给shared_ptr的构造函数,常见替代手法是使用std::make_shared,但是使用自定义析构器就无法使用make_shared。如果要传递裸指针,也就直接传递new运算符的结果。
  11. 当我们希望一个托管到shared_ptr的类能够安全地由this指针创建一个shared_ptr时,std::enable_shared_from_this将为我们继承而来的基类提供一个模板。std::enable_shared_from_this是一个基类模板,其类型形参总是其派生类的类名。这种设计模式的名字叫做奇妙递归模板模式。
  12. std::enable_shared_from_this定义了一个成员函数,它会创建一个shared_ptr指向当前对象,但同时不会重复创建控制块。
  13. 与unique_ptr不同,shared_ptr仅被设计用来处理指向单个对象的指针,并没没有所谓的std::shared_ptr(T[])。

对于类似std::shared_ptr但有可能空悬的指针使用std::weak_ptr

  1. std::weak_ptr一般是通过shared_ptr来创建的,当使用shared_ptr完成初始化weak_ptr的时候,两者就指向了同一位置,但是weak_ptr并不会影响指向对象的引用计数。
  2. 当shared_ptr的引用计数为0时,指向同一对象的weak_ptr将会空悬,也叫做失效,可以使用expired成员函数来检测是否失效。
  3. 若想在weak_ptr未失效的情况下访问其指向的对象,可以使用两者方法,一种是lock方法:std::shared_ptr<widget> spw=wpw.lock(),此时若wpw失效则spw为空。另外一种是使用weak_ptr作为实参构造shared_ptr,此时若wpw失效则会抛出异常。
  4. 当两个对象互相持有指向对方的指针,有一方是shared_ptr。
    如果回指指针是裸指针的话,当另一对象被析构对其解引用则会引发未定义行为
    如果回指指针是shared_ptr的话则会使得这两个shared_ptr指向的对象即双方对象的引用计数永远至少为1,永远不会被析构从而造成内存泄露
    而如果回指指针是weak_ptr的话假设这一对象被析构,则此weak_ptr将会空悬我们也能察觉到这一问题。而且不会影响引用计数,也就不会组织对象被析构
  5. std::weak_ptr可能的用武之地包括缓存,观察者列表以及避免shared_ptr指针回路。

优先选用std::make_unique和std::make_shared,而非直接使用new

  1. make系列函数会把一个任意实参集合完美转发给动态分配内存的对象的构造函数,并返回一个指向该对象的智能指针。
  2. make系列函数的第三个是std::allocate_shared,它的行为与std::make_shared相同,只不过它的第一个实参是个用以动态分配内存的分配器对象。
  3. 当new运算符动态分配了内存但是之后发生异常导致没有将new表达式的结果作为shared_ptr的构造函数实参,此时将会发生内存泄漏。使用make_shared可以避免该问题。
  4. 在运行期,传递给函数的实参必须在函数调用被发起前完成评估求值,但是各个实参完成评估求值的结果却是不一定的。
  5. 与直接使用new表达式相比,make_shared还能带来性能的提升,使用new表达式来初始化shared_ptr会发生两次内存分配,分别是new一次以及为与其相关的控制块再进行一次内存分配。而使用make_shared则只需要一次内存分配就会分配单块内存既保存对象又保存与其相关联的控制块。这个优势同样适用于std::allocate_shared。
  6. 所有的make系列函数都不允许使用自定义析构器,欲创建一个使用该自定义析构器的智能指针,直接使用new表达式即可。
  7. 由于make系列函数会向对象的构造函数完美转发其形参,但由于不能够完美转发大括号初始化物,因此假如需要使用大括号初始化物来创建指向对象的指针时就必须使用new表达式了,此时也能由大括号初始化物创建一个std::initializer_list对象,然后再将此对象传递给make函数。
  8. 由于控制块的存在,使用shared_ptr以及其make函数去为一个带有自定义版本的operator new和operator delete的类创建对象通常不是一个好主意。
  9. 使用make_shared函数会将对象和控制块分配在同一块内存上,但是当对象引用计数为0时对象析构,而此时如果像弱引用计数之类的尚不为0则控制块不会析构,因此对象和控制块所占内存不会释放。假如对象尺寸较大,且最后一个shared_ptr和最后一个weak_ptr析构之间的时间不能忽略,在对象的析构和内存的释放就会产生延迟。而直接使用new表达式则会在对象析构的时候就释放该对象所占的内存。
  10. 如果初始化shared_ptr不能够或者不适合使用make_shared时,为了避免异常安全的问题,最好的办法就是直接使用new表达式的结果传递给智能指针的构造函数,并且在这一条语句中不要做其他任何事情。
  11. 当函数形参是右值时,如果实参是右值,仅需要一次移动即可,而如果实参是左值还需要进行一次复制,当复制操作成本比较高时,可以使用std::move()函数将左值转换为右值。
  12. 对于std::shared_ptr,不建议使用make系列函数的额外场景包括:
    ①自定义内存管理的类
    ②内存紧张的系统、非常大的对象、以及存在比指向相同对象的shared_ptr更长生存期的weak_ptr

使用Pimpl习惯用法时,将特殊成员函数的定义放到实现文件中

  1. Pimpl:pointer to implementation,即指向实现的指针,这种技巧就是把某类的数据成员用一个指向到某实现类(或结构体)的指针替代,然后将原来在主类中的数据成员放置在实现类中,并通过指针间接访问这些数据成员。
  2. 使用Pimpl这一用法将减少头文件需要include的头文件,提升编译速度,同时即使头文件内容进行了修改这一文件也无需重新编译。
  3. 一个已声明但未定义的类型称为非完整类型,非完整类型可以做的事极其有限,但是可以声明一个指向其的指针。
  4. Pimpl习惯用法的第一部分是声明一个指针类型的数据成员,指向一个非完整类型,第二部分是动态分配和回收持有从前在原始类里的那些数据成员的对象,而分配和回收代码都放在实现文件中。
  5. 使用裸指针实现Pimpl习惯用法时需要我们自己定义析构函数从而回收动态分配的内存,但是如果使用智能指针则无需如此,但是使用智能指针时需要注意默认析构器是使用delete运算符来针对裸指针实施析构函数,而在实施delete之前典型实现会使用11中的static_assert去确保裸指针未指向非完整类型。要解决这一问题,只需保证在编译器生成析构智能指针代码处知道我们所定义的智能指针指向的对象是个完整类型即可。因此成功编译的关键在于让编译器看到析构函数的函数体的位置在实现文件中智能指针指向的对象的定义之后即可,只需在类声明中声明析构函数然后在实现文件中实现类的定义之后再定义它。
  6. 使用了智能指针实现了Pimpl习惯用法的类,想要实现移动操作的话,编译器会在move构造函数内抛出异常的事件中生成析构智能指针的代码,而对智能指针的析构要求其指向的实现类是完整类型。因此解决方法也是类中声明移动操作,然后在实现文件中实现类的定义后再使用移动操作的默认default定义。
  7. 如果想要让Pimpl习惯用法实现的类支持复制操作,我们需要自己写这些函数,因为:
    ①编译器不会为unique_ptr那样的只移类别生成复制操作
    ②即使编译器可以生成,其生成的函数也只能复制unique_ptr(浅复制),而我们希望则是复制指针所指向的内容(即实施深复制)。
    我们在头文件中声明这些函数,并在实现文件中实现它们。
  8. 为达到Pimpl习惯用法的目的,应该是用的是unique_ptr,因为对象内部的指针拥有相应的实现对象的专属所有权,如果使用shared_ptr的话上面需要注意的问题都不用注意,因为析构器类别不是shared_ptr类型的一部分,在使用编译器生成的特种函数时shared_ptr不要求指向的类型是完整类型。
  9. Pimpl习惯用法通过降低类的客户和类实现者之间的依赖性,减少了构建遍数。