• C++智能指针(二)——weak_ptr初探



    1. shared_ptr 存在的问题

    shared_ptr 的引入要解决普通指针存在的一些问题一样,weak_ptr 的引入,也是因为 shared_ptr 本身在某些情况下,存在一些问题或有一些不完善的地方,考虑以下两个场景:

    • 循环引用(cyclic references)。如果两个对象使用 shared_ptrs 互相引用,那么就算将两个对象指针设为nullptr,此时理应释放资源,但由于内部的循环引用,此时 shared_ptrs 的 use_count() = 1,导致并不会释放资源

      下面为循环引用的一个具体示例代码:
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    class Person {
    public:
    string name;
    shared_ptr<Person> mother;
    shared_ptr<Person> father;
    vector<shared_ptr<Person>> kids;
    Person (const string& n,
    shared_ptr<Person> m = nullptr,
    shared_ptr<Person> f = nullptr)
    : name(n), mother(m), father(f) {
    }
    ~Person() {
    cout << "delete " << name << endl;
    }
    };
    shared_ptr<Person> initFamily (const string& name)
    {
    shared_ptr<Person> mom(new Person(name+"’s mom"));
    shared_ptr<Person> dad(new Person(name+"’s dad"));
    shared_ptr<Person> kid(new Person(name,mom,dad));
    mom->kids.push_back(kid);
    dad->kids.push_back(kid);
    return kid;
    }
    int main()
    {
    shared_ptr<Person> p = initFamily("nico");
    cout << "nico’s family exists" << endl;
    cout << "- nico is shared " << p.use_count() << " times" << endl;
    cout << "- name of 1st kid of nico’s mom: "
    << p->mother->kids[0]->name << endl;
    p = initFamily("jim");
    cout << "jim’s family exists" << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    首先,initFamily() 创建了三个Person对象:mon,dad 和 kid。kid 使用了 mom 和 dad 的共享指针进行创建。mom 和 dad 也将 kid 共享指针插入到 vector 中,最后将 kid 指针返回给 p,initFamily() 调用完结果如下图所示。

    kid 有指向 mom 和 dad 的指针,mom 和 dad 中也有指向 kid 的指针,此时循环引用就产生了。因此这里 p 的 use_count=3,所以当赋值一个新的Person给p或者让p为nullptr,或者在 main() 末尾离开了 p 的作用域 —— 没有 Person 对象会被释放,因为每个至少有一个指针指向,因此输出 delete name 永远不会调用,实际输出如下:

    nico’s family exists
    - nico shared 3 times
    - name of 1st kid of nicos mom: nico
    jim’s family exists
    
    • 1
    • 2
    • 3
    • 4
    • 如果只是想共享而不是想拥有对象。即一个指针的生命周期要长于指向对象的生命周期。此时使用 shared_ptrs 会导致无法释放资源,使用普通指针存在访问释放资源的风险,后续对weak_ptr使用的讲解中进一步说明。

    2. 使用weak_ptr

    鉴于上面 shared_ptr 存在的问题,C++11 提供了 weak_ptr 类,允许共享对象,但并不实际拥有对象,这个类需要传入一个共享指针来创建。当最后一个共享指针失去对象所有权(要释放空间与资源了),共享对象的 weak_ptr 自动设为空(本来就没有对象的所有权,自然也不负责对于空间与资源的释放) 。

    我们使用 weak_ptr 改写上面的代码:

    class Person {
    public:
    string name;
    shared_ptr<Person> mother;
    shared_ptr<Person> father;
    vector<weak_ptr<Person>> kids; // weak pointer !!!
    Person (const string& n,
    shared_ptr<Person> m = nullptr,
    shared_ptr<Person> f = nullptr)
    : name(n), mother(m), father(f) {
    }
    ~Person() {
    cout << "delete " << name << endl;
    }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    通过使用 weak_ptr 打破共享指针的循环引用,只有 kid 指向父母的指针使用共享指针,父母指向 kid 的指针使用(下图中的虚线)

    这样 p 的 use_coun=1,所以 p 删除时,会释放对应的内存和资源。程序输出如下:

    nico’s family exists
    - nico shared 1 times
    - name of 1st kid of nicos mom: nico
    delete nico
    delete nico’s dad
    delete nico’s mom
    jim’s family exists
    delete jim
    delete jim’s dad
    delete jim’s mom
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    下面详细讲解 weak_ptr 的使用

    2.1 初始化 weak_ptr

    因为 weak_ptr 只能使用 shared_ptr 初始化,所以 weak_ptr 只提供了默认构造函数、拷贝构造函数以及传入 shared_ptr 的构造函数,因为不是显式构造函数,所以可以在 vector 中直接插入共享指针(隐式转换):

    mom->kids.push_back(kid);
    dad->kids.push_back(kid);
    
    • 1
    • 2

    2.2 访问数据

    之前使用 shared_ptr 访问 vector 中共享指针指向的数据使用以下语法:

    p->mother->kids[0]->name
    
    • 1

    而对于 weak_ptr 则要使用如下语法:

    p->mother->kids[0].lock()->name
    
    • 1

    lock() 获取共享指针,。如果在 lock 获取共享指针时,资源已经被释放了,则返回空的 shared_ptr

    此时,再调用操作符 *-> 都会产生未定义行为。

    因此,最好在获取共享指针前,首先对资源是否释放进行检查,有如下 3 种方法:

    1. 调用 expired() 方法,如果 weak_ptr 不再共享一个对象则返回 true。这与检查 use_count() 是否等于 0 是等价的,但可能运行速度更快
    2. 可以显式将 weak_ptr 使用对应构造函数转换为 shared_ptr。如果此时没有合法的引用对象,则这个构造函数抛出一个 bad_weak_ptr 异常。 这是一个派生自 std::exception 的一个异常,what() 返回 bad_weak_ptr (每个设备上实现有所差异)。
    3. 可以调用 use_count() 查询关联对象所有者的数量。如果返回值是 0,这将不会再有合法对象。这个方法最好只是在debug时使用,因为效率不高

    三种方法的具体代码如下:

    try {
    shared_ptr<string> sp(new string("hi")); // create shared pointer
    weak_ptr<string> wp = sp; // create weak pointer out of it
    sp.reset(); // release object of shared pointer
    cout << wp.use_count() << endl; // prints: 0
    cout << boolalpha << wp.expired() << endl; // prints: true
    shared_ptr<string> p(wp); // throws std::bad_weak_ptr
    }
    catch (const std::exception& e) {
    cerr << "exception: " << e.what() << endl; // prints: bad_weak_ptr
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3. 附录

    A. weak_ptr 操作列表


    4. 参考文献

    《The C++ Standard Library》A Tutorial and Reference, Second Edition, Nicolai M. Josuttis.

  • 相关阅读:
    系统设计.秒杀系统
    运维基础环境搭建
    Linux从 全栈开发 centOS 7 到 运维
    Paper Survey——3DGS-SLAM
    400G QSFP-DD SR8光模块应用场景解析
    Java学习笔记4.5.2 日期时间 - JDK8新增日期与时间类
    Python拆分列中文和 字符
    OC中转化成Model 和 普通的NString获取值的不同
    有向图的表示与动态选择算法
    JVM中的STW(Stop The World)
  • 原文地址:https://blog.csdn.net/weixin_44491423/article/details/133784575