C++智能指针

qilingzhaoqilingzhao
2 min read

std::unique_ptr

对于独占资源(exclusive-ownership resource)使用std::unique_ptr,它禁止和其他智能指针共享对象

使用

std::shared_ptr只有单个对象形式不同, std::unique_ptr有两种形式, 对于单个对象(std::unique_ptr<T>), 对于数组(std::unique_ptr<T[]>), 对于解引用操作符(operator*operator->)对于单个对象专有。

std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入
std::unique_ptr<int> pointer2 = pointer; // 非法

C++ 并没有提供 std::make_unique, 可自行实现

template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args ) {
  return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
}

类型

对于std::unique_ptr来说,删除器是类型的一部分。

占用空间

当使用默认删除器(不使用自定义删除器)的时候,std::unique_ptr的对象大小和原始指针大小相同,都为一个字(word)。当使用自定义删除器的时候,对于无状态(stateless function)的对象,不捕获变量的lambda表达式(下面的del_func), std::unique_ptr大小增加为两个字(word);当捕获外部变量时,std::unique_ptr的大小取决于,函数对象中存储的状态。

void func_unique_ptr_size() {
    cout << "ptr size:" << sizeof(new Weight()) << endl;
    unique_ptr<Weight> default_del_unique_ptr(new Weight());
    cout << "default_del_unique_ptr size: " << sizeof (default_del_unique_ptr) << endl;

    auto del_func = [](Weight* w){delete w;};
    unique_ptr<Weight, decltype(del_func)> custom_delete_func_unique_ptr(new Weight(), del_func);
    cout << "custom_delete_func_unique_ptr size: " << sizeof(custom_delete_func_unique_ptr) << endl;

    int i = 100;
    auto del_func_stateful = [&i](Weight* w) {int x = i; delete w;};
    unique_ptr<Weight, decltype(del_func_stateful)> custom_stateful_del_func_unique_ptr(new Weight, del_func_stateful);
    cout << "custom_stateful_del_func_unique_ptr size: " << sizeof(custom_stateful_del_func_unique_ptr) << endl;
}
输出
ptr size:8
default_del_unique_ptr size: 8
custom_delete_func_unique_ptr size: 8
custom_stateful_del_func_unique_ptr size: 16

std::shared_ptr

对于共享资源(shared-ownership resource) 使用 std::shared_ptr

使用

std::shared_ptr能够记录有多少个std::shared_ptr指向同一个对象,当引用计数为0的时,会自动调用delete删除对象。

可以通过get()获取原始指针。通过use_count()来查看一个对象的引用计数。当智能指针中有值时,通过reset()来减少一个引用计数,调用 reset(new xxx())重新赋值,并且引用计数置为1。

内部实现

std::shared_ptr的大小是原始指针的两倍,包含一个资源的原始指针,和指向控制块(control block)的指针。当使用原始指针创建std::shared_ptr时,控制块的内存会动态分配,使用make_shared可以避免。

image.png

当使用自定义删除器时,不会改变shared_ptr的大小,因为自定义删除器保存在shared_ptr内指针指向的内存。

控制块创建的时机

  • 使用make_shared时总会创建一个控制块
    shared_ptr<Weight> sp0 = std::make_shared<Weight>()
    
  • 从独占指针unique_ptr上构造shared_ptr时会创建控制快

    unique_ptr<Weight> uniquePtr(new Weight());
    shared_ptr<Weight> sp1 = move(uniquePtr);
    cout << uniquePtr << endl; // 0x0
    
  • 在原始指针上构造shared_ptr会创建控制块。但如果从已经存在控制块的对象上创建shared_ptr不会创建新的控制块,可以依赖传递来的智能指针指向控制块。

    shared_ptr<Monster> spM0(new Monster("m0"));
    cout << spM0.use_count() << endl; // 1
    shared_ptr<Monster> spM1(spM0); // 此处不创建新的控制块, spM1的指向控制快地址和spM0相同
    cout << spM0.use_count() << endl; // 2
    

    image.png

image.png

使用注意点

  1. 当在一个原始指针上创建多个shared_ptr,也就是说由多个具有不同的控制块shared_ptr管理同一份资源。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数一次)
    auto pw = new Widget;                           //pw是原始指针
    std::shared_ptr<Widget> spw1(pw, loggingDel);   //为*pw创建控制块
    std::shared_ptr<Widget> spw2(pw, loggingDel);   //为*pw创建第二个控制块
    
    因此,在创建shared_ptr时,最好使用make_shared, 不过这样无法自定义删除器。必须给shared_ptr构造函数原始指针时,直接传递 new 出来的匿名对象。
  2. 如果你想创建一个用std::shared_ptr管理的类,这个类能够用this指针安全地创建一个std::shared_ptr,std::enable_shared_from_this就可作为基类的模板类。
    class Widget: public std::enable_shared_from_this<Widget> {
    public:
     …
     void process();
     …
    };
    
  3. shared_ptr不支持数组对象std::shared_ptr<T[]>

std::weak_ptr

当std::shard_ptr可能悬空(dangle)时使用std::weak_ptr weak_ptr是一个可以不需要手动管理内存,同时不参与资源所有权共享的智能指针。它不是一个可以独立存在的指针,是shared_ptr的增强。

使用

  • 可以在shared_ptr上创建weak_ptr, 在两个指针上使用use_count()获取到的都是指向资源的shared_ptr的个数
    shared_ptr<Weight> sp = make_shared<Weight>();
    weak_ptr<Weight> wp0(sp);
    cout << sp.use_count() << endl; // 1
    weak_ptr<Weight> wp1(sp);
    cout << sp.use_count() << endl; // 1
    cout << wp1.use_count() << endl; // 1
    
  • 可以通过一个原子操作检查weak_ptr是否已经过期,如果没有过期就访问指向对象。可以通过在weak_ptr上创建shared_ptr实现。有两种方式:
    shared_ptr<Weight> sp1 = wp0.lock();
    if (sp1 == nullptr) {
      cout << "dangle" << endl;
    }
    shared_ptr<Weight> sp2(wp0); // 如果wp0过期, 则抛出std::bad_weak_ptr异常
    
  • 打破std::shared_ptr环状结构

使用 std::make_shared 和 std::make_unique

优点(建议使用的场景)

  1. 避免类型重复
    auto upw1(std::make_unique<Widget>());      //使用make函数, 只写Weight一次类型
    std::unique_ptr<Widget> upw2(new Widget);   //不使用make函数, 需要写两次
    
  2. 异常安全(exception safe) 对于函数void processWidget(std::shared_ptr<Widget> spw, int priority);有一个函数int computePriority();计算优先级, 下面代码存在潜在的内存泄露。
    processWidget(std::shared_ptr<Widget>(new Widget),  computePriority());
    
    一个函数的实参必须先被计算,这个函数再被调用, new Widget必须在std::shared_ptr的构造函数被调用前执行,因为new出来的结果作为构造函数的实参,但computePriority可能在这之前,之后,或者之间执行。
    执行“new Widget”
    执行computePriority // 如果在这里抛出异常,会导致 new Weiget的资源无法释放。
    运行std::shared_ptr构造函数
    
  3. 效率提升 对于std::shared_ptr<Widget> spw(new Widget);需要进行两次内存分配。一次使用new分配Weight的内存。 一次为shared_ptr分配控制块内存。 使用make_shared增加内存分配效率,并且潜在的减少了程序的总内存占用。

不足(不建议使用的场景)

  1. 自定义删除器
    auto widgetDeleter = [](Widget* pw) { … };
    std::shared_ptr<Widget> spw(new Widget, widgetDeleter);
    
  2. 使用花括号初始化

ref

https://www.cnblogs.com/KillerAery/p/9096558.html https://juejin.cn/post/6844903993055920141#heading-14 https://heleifz.github.io/14696398760857.html https://cntransgroup.github.io/EffectiveModernCppChinese/4.SmartPointers/item19.html https://changkun.de/modern-cpp/zh-cn/05-pointers/

0
Subscribe to my newsletter

Read articles from qilingzhao directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

qilingzhao
qilingzhao