简介
裸指针的缺陷:
- 未指明是指针还是数组
- 裸指针无法提示在使用完之后是否需要析构
- 即使知道析构函数所指的函数也不知道如何析构合适,是delete还是传入专门的用于析构的函数
- 即使知道了应该使用delete,但是不知道使用delete还是delete[]。一旦用错会导致未定义行为
- 即使知道如何析构,但是需要保证所有路径只执行一次,如果未执行则发生泄漏,执行多次则发生未定义行为
- 没办法检测出裸指针是否空悬
4种智能指针:
auto_ptr:已经弃用,后面全部都使用unique。使用了复制操作符实现了移动语义,导致复制的时候会被置空。并且无法在容器中使用
unique_ptr:可以做auto_ptr 的所有事情。并且去除了限制。
shared_ptr:引用计数
weak_ptr:防止循环引用,用于有可能空悬的指针
条款18. 使用std::unique_ptr管理具备专属权的资源
- unique_ptr是只移类型的只能指针,对于托管资源采用的是专属所有权。
- 一般来说unique_ptr的析构都使用默认的delete。不过也可以指定自定义的删除器,不过这会造成指针尺寸变大。
auto del = [](Object* obj)
{
//do something..
delete obj;
};
// 使用时可以使用以下类型
// std::unique_ptr<Object, decltype(del)>
- unique_ptr可以直接转换为shared_ptr,不过反过来不行。
- 裸指针不允许直接赋值给unique_ptr,会出现比较大的问题
- 不要对数组使用unique_ptr,而是使用适合的stl容器
条款19.使用std::shared_ptr管理具备共享所有权的资源
性能影响
- std::shared_ptr的尺寸是裸指针的两倍(包含一个控制块的指针)
- 引用计数的内存是动态分配的
- 引用计数的递增递减是原子操作
自定义删除器
shared_ptr也可以自定义删除行为,但是与unique_ptr相比,自定义删除行为并不属于类的一部分,所以更具弹性。
auto loggingDel = [](Widget* pw){
// do something...
delete pw;
};
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
// 模板无需传入类型
std::shared_ptr<Widget> spw(new Widget, loggingDel);
自定义析构器不会改变shared_ptr的大小,因为引用计数、弱计数、自定义删除器、分配器等是保存在控制块上,而shared_ptr只拿了其指针。
控制块何时创建
- std::make_shared总是创建一个控制块。
- 从专属所有权指针(unique_ptr或auto_ptr)转换为shared_ptr时会创建一个控制块
- 如果从裸指针创建shared_ptr时(虽然一般我们不建议这么做),会创建一个控制块
多控制块控制单一裸指针陷阱
如果一个指针同时创建了两个shared_ptr,那么有两个控制块同时控制一个裸指针,会导致两次析构,引向了未定义行为的结果。
所以一般建议这样做:
std::shared<Widget> spw1(new Widget, loggingDel);
还有一个容易出现的问题是容器中使用了shared_ptr
// 和直接用裸指针创建shared_ptr没有什么区别
processedWidgets.emplace_back(this);
我们可以使用shared_from_this()的接口来避免这个问题。
通过继承enable_shared_from_this<T>,来帮助用户使用shared_ptr
条款20.对于类似std::shared_ptr但是有可能空悬的指针使用std::weak_ptr
通过lock方法我们可以通过weak_ptr判断指针是否失效。
互相引用的指针
- 直接使用裸指针,容易产生未定义行为
- 使用shared_ptr,循环引用,资源无法回收
- weak_ptr,可以防止以上的两种情况
可能的使用场景
本地缓存、观察者列表,避免循环引用
如果是父子类的情况,实际上子类的声明周期一般都小于父类,所以子类直接使用裸指针也还行。
条款21.优先选用std::make_unique和std::make_shared,不是直接用new
make函数的优点
- shared_ptr可以避免两次分配,而只需要一次动态分配
- 可以统一申请对象的写法,目标代码也更小
- 可以避免在构造函数调用之前发生异常导致的泄漏
应该使用构造函数而不是make函数的情况
- 需要定制删除器
- 希望直接传递初始化列表
make_shared不适合的地方
- 自定义内存管理的类
- 内存紧张的系统,非常大的对象。有可能导致内存释放的延迟。
条款22.使用Pimpl习惯用法时,将特殊成员函数定义到实现文件中
所谓Pimple就是在类中定义一个额外的实现类,然后实际使用的对外接口只取这个实现类的指针。
// widget.h
class Widget{
public:
Widget();
**~Widget();//必须定义,否则无法编译通过,因为unique_ptr的删除器是类的一部分,必须可达
//声明析构之后会组织移动语义生成,故需要主动声明移动语义
Widget(Widget&& rhs);
Widget& operator=(Widget&& rhs);**
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
}
实现文件如下
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl{
std::string name;
std::vector<double> data;
Gadget g1,g2,g3;
}
Widget::Widget()
: pImpl(std::make_unique<Impl>)
{}
Widget::~Widget() = default;
**Widget(Widget&& rhs) = default;
Widget& operator=(Widget&& rhs) = default;**
以上要求仅针对unique_ptr