ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【C++面试】手写智能指针(三):weak_ptr如何解决shared_ptr循环引用

【C++面试】手写智能指针(三):weak_ptr如何解决shared_ptr循环引用 摘要一、为什么有了shared_ptr还需要weak_ptr前一篇已经介绍过shared_ptr ↓ 多个对象共同拥有资源 ↓ 通过引用计数管理生命周期例如MySharedPtrint p1(new int(10)); MySharedPtrint p2 p1;此时p1 ──┐ ├──→ 10 p2 ──┘ strong_count 2只要strong_count ! 0资源就不会释放。正常情况下这没有问题。但是如果两个对象互相保存shared_ptr就可能产生循环引用例如class B; class A { public: std::shared_ptrB b; }; class B { public: std::shared_ptrA a; };然后std::shared_ptrA pa std::make_sharedA(); std::shared_ptrB pb std::make_sharedB(); pa-b pb; pb-a pa;关系变成shared_ptr A ─────────────────→ B ↑ │ │ │ └────────────────────┘ shared_ptr这时候A持有B B又持有A假设外部pa pb离开作用域。正常来说应该释放A B但现在A仍然被B中的shared_ptr引用 B仍然被A中的shared_ptr引用所以两个对象的强引用计数始终无法降到0最终A无法析构 B无法析构造成内存泄漏这就是shared_ptr循环引用而weak_ptr就是专门用来解决这种问题的。二、weak_ptr和shared_ptr最大的区别是什么weak_ptr最重要的特点是它可以指向一个由shared_ptr管理的对象但是不会增加强引用计数。例如shared_ptr ↓ 拥有资源而weak_ptr ↓ 只观察资源可以把它们理解成shared_ptr ↓ “这个对象是我的我参与决定它什么时候销毁” weak_ptr ↓ “我知道这个对象在哪里但它什么时候销毁和我无关”假设std::shared_ptrint sp std::make_sharedint(10);此时strong_count 1再创建std::weak_ptrint wp sp;不会变成strong_count 2而仍然是strong_count 1因为wp不拥有资源它只是在观察。所以可以理解成sp ↓ 资源10 wp ↓ 观察资源10当sp销毁以后strong_count 1 → 0资源就应该被释放。即使wp还存在也不能阻止资源析构。这正是weak_ptr能够解决循环引用的原因。例如把前面的class B { public: std::shared_ptrA a; };改成class B { public: std::weak_ptrA a; };关系就变成A ─────────────→ B ↑ │ │ │ └ - - - - - - - ┘ weak_ptr其中A → B是强引用。而B → A只是弱引用。所以B中的weak_ptr不会增加A的strong_count外部shared_ptr销毁以后引用计数就可以正常归零。三、手写weak_ptr为什么需要控制块如果只手写unique_ptr只保存一个T *ptr_;就够了。如果手写简化版shared_ptr上一篇用了T *ptr_; size_t *count_;表示对象地址 共享引用计数但是加入weak_ptr以后就不能只保存一个strong_count了。因为现在需要同时记录有多少个shared_ptr 有多少个weak_ptr所以更合理的设计是引入控制块 Control Block例如template typename T struct ControlBlock { T *ptr; size_t strong_count; size_t weak_count; explicit ControlBlock(T *p) : ptr(p), strong_count(1), weak_count(0) { } };这里strong_count记录shared_ptr数量而weak_count记录weak_ptr数量整个结构可以理解成ControlBlock ┌──────────────┐ shared_ptr ───────→│ ptr │────→ Object │ strong_count │ weak_ptr ─────────→│ weak_count │ └──────────────┘这样shared_ptr weak_ptr都不用自己单独保存引用计数。而是一起指向同一个ControlBlock例如sp1 ─────┐ │ sp2 ─────┼────→ ControlBlock ────→ Object │ wp1 ─────┤ │ wp2 ─────┘控制块里面strong_count 2 weak_count 2这种结构已经比较接近真正shared_ptr/weak_ptr的设计思路了。四、手写一个简化版MySharedPtr和MyWeakPtr为了让weak_ptr能够真正工作这一篇把shared_ptr稍微改造成控制块版本先定义控制块#include cstddef #include iostream #include utility template typename T struct ControlBlock { T *ptr; size_t strong_count; size_t weak_count; explicit ControlBlock(T *p) : ptr(p), strong_count(1), weak_count(0) { } };然后先声明template typename T class MyWeakPtr;因为后面的MySharedPtr需要和MyWeakPtr互相访问控制块。1. MySharedPtrtemplate typename T class MySharedPtr { private: ControlBlockT *ctrl_; void release() { if (!ctrl_) return; // 当前shared_ptr数量减一 --ctrl_-strong_count; // 没有shared_ptr以后真正释放对象 if (ctrl_-strong_count 0) { delete ctrl_-ptr; ctrl_-ptr nullptr; // 如果连weak_ptr也没有控制块也可以释放 if (ctrl_-weak_count 0) { delete ctrl_; } } ctrl_ nullptr; } // 允许weak_ptr访问控制块 friend class MyWeakPtrT; // 提供给lock()使用 explicit MySharedPtr(ControlBlockT *ctrl) : ctrl_(ctrl) { if (ctrl_) ctrl_-strong_count; } public: explicit MySharedPtr(T *ptr nullptr) : ctrl_(ptr ? new ControlBlockT(ptr) : nullptr) { } ~MySharedPtr() { release(); } MySharedPtr(const MySharedPtr other) : ctrl_(other.ctrl_) { if (ctrl_) ctrl_-strong_count; } MySharedPtr operator(const MySharedPtr other) { if (this ! other) { release(); ctrl_ other.ctrl_; if (ctrl_) ctrl_-strong_count; } return *this; } MySharedPtr(MySharedPtr other) noexcept : ctrl_(other.ctrl_) { other.ctrl_ nullptr; } MySharedPtr operator(MySharedPtr other) noexcept { if (this ! other) { release(); ctrl_ other.ctrl_; other.ctrl_ nullptr; } return *this; } T operator*() const { return *(ctrl_-ptr); } T *operator-() const { return ctrl_-ptr; } T *get() const { return ctrl_ ? ctrl_-ptr : nullptr; } size_t use_count() const { return ctrl_ ? ctrl_-strong_count : 0; } explicit operator bool() const { return ctrl_ ctrl_-ptr; } };这里和上一篇最大的区别就是原来T *ptr_; size_t *count_;现在变成ControlBlockT *ctrl_;也就是MySharedPtr ↓ ControlBlock ↓ 真正对象当strong_count 0时释放对象delete ctrl_-ptr;但是控制块不能马上一定删除。因为可能还有weak_ptr正在观察这个控制块。所以只有strong_count 0 并且 weak_count 0控制块才能真正删除。2. MyWeakPtr接下来实现MyWeakPtr它不需要保存T *ptr_只需要知道ControlBlock在哪里。template typename T class MyWeakPtr { private: ControlBlockT *ctrl_; void release() { if (!ctrl_) return; --ctrl_-weak_count; // 对象早就没了并且现在也没有weak_ptr了 if (ctrl_-strong_count 0 ctrl_-weak_count 0) { delete ctrl_; } ctrl_ nullptr; } public: MyWeakPtr() : ctrl_(nullptr) { } // 从shared_ptr创建weak_ptr MyWeakPtr(const MySharedPtrT shared) : ctrl_(shared.ctrl_) { if (ctrl_) ctrl_-weak_count; } // weak_ptr拷贝 MyWeakPtr(const MyWeakPtr other) : ctrl_(other.ctrl_) { if (ctrl_) ctrl_-weak_count; } ~MyWeakPtr() { release(); } MyWeakPtr operator(const MyWeakPtr other) { if (this ! other) { release(); ctrl_ other.ctrl_; if (ctrl_) ctrl_-weak_count; } return *this; } bool expired() const { return !ctrl_ || ctrl_-strong_count 0; } MySharedPtrT lock() const { // 对象已经不存在 if (expired()) return MySharedPtrT(); // 临时得到一个新的shared_ptr return MySharedPtrT(ctrl_); } size_t use_count() const { return ctrl_ ? ctrl_-strong_count : 0; } };这里最重要的两个接口就是expired()和lock()expired()用来判断原来的对象还在不在例如if (wp.expired()) { std::cout object destroyed std::endl; }判断的是strong_count 0因为强引用计数为0就表示已经没有shared_ptr拥有资源了。对象已经被释放。而lock()的作用是如果对象还活着 临时获得一个shared_ptr例如auto sp wp.lock();如果成功weak_ptr ↓ lock() ↓ 得到shared_ptr ↓ strong_count 1这样在sp存在期间对象就不会突然被释放。使用示例int main() { MyWeakPtrint wp; { MySharedPtrint sp(new int(100)); wp MyWeakPtrint(sp); std::cout count sp.use_count() std::endl; MySharedPtrint temp wp.lock(); if (temp) { std::cout value *temp std::endl; } } if (wp.expired()) { std::cout object already destroyed std::endl; } return 0; }整个过程可以理解成创建sp ↓ strong_count 1 weak_count 0 ↓ 创建wp ↓ strong_count 1 weak_count 1 ↓ wp.lock() ↓ 临时创建shared_ptr ↓ strong_count 2 ↓ 临时shared_ptr析构 ↓ strong_count 1 ↓ 原sp析构 ↓ strong_count 0 ↓ 对象释放 ↓ weak_ptr仍然存在 ↓ ControlBlock继续存在 ↓ wp.expired() true注意对象已经销毁并不代表控制块马上销毁因为weak_ptr还需要通过控制块知道这个对象已经不存在了所以这两个生命周期要分开对象生命周期 ↓ 由strong_count决定 控制块生命周期 ↓ 由strong_count和weak_count共同决定这就是shared_ptr weak_ptr设计中非常关键的地方。五、weak_ptr面试最容易追问什么第一个最常见的问题就是weak_ptr为什么能够解决循环引用因为weak_ptr不会增加strong_count例如class B; class A { public: MySharedPtrB b; }; class B { public: MyWeakPtrA a; };关系A ─────────────→ B ↑ │ │ │ └ - - - - - - - ┘ weak假设外部pa → A pb → B然后A持有B的shared_ptr所以 B 有一个强引用。但是B持有A的weak_ptr并不会增加 A 的强引用计数。当外部pa释放以后A 的强引用计数可以正常变成0于是 A 能够析构。A 析构以后它内部的shared_ptrB也跟着析构。于是 B 的强引用计数继续减少。最终B也可以正常释放循环就被打破了。第二个常见问题是为什么weak_ptr不能直接使用operator-因为weak_ptr不拥有资源。假设wp-show();如果刚好在这一瞬间最后一个shared_ptr销毁那么对象已经释放。此时直接使用wp就不安全。所以标准方式是auto sp wp.lock(); if (sp) { sp-show(); }lock()成功以后strong_count 1于是在sp生命周期内 对象一定还活着所以正确思路是weak_ptr ↓ 不能直接拥有对象 ↓ lock() ↓ 临时转成shared_ptr ↓ 安全访问对象第三个常见问题是expired()和lock()分别干什么可以简单回答expired() ↓ 判断资源是否已经释放 lock() ↓ 如果资源还存在 返回一个shared_ptr 否则返回空shared_ptr实际代码更推荐直接if (auto sp wp.lock()) { sp-show(); }而不是if (!wp.expired()) { // 再使用 }因为在多线程环境下expired检查完成 ↓ 对象可能马上被其他线程释放而lock()的目标就是直接尝试获得一个新的强引用。第四个常见问题是strong_count为0以后为什么ControlBlock不能立即释放因为weak_ptr还可能存在。例如strong_count 0 weak_count 3虽然真正对象已经销毁但这三个weak_ptr仍然需要知道对象已经失效它们需要继续访问ControlBlock判断expired()所以strong_count 0只代表释放T对象而strong_count 0 weak_count 0才表示ControlBlock也可以释放可以简单记成strong_count ↓ 控制对象什么时候销毁 weak_count ↓ 影响控制块什么时候销毁第五个问题是shared_ptr、weak_ptr和控制块是什么关系可以用一张图直接理解shared_ptr1 ─────┐ │ shared_ptr2 ─────┤ ↓ ┌──────────────┐ │ ControlBlock │ │ │ │ strong 2 │ │ weak 1 │ └──────┬───────┘ │ ↓ Object ↑ │ weak_ptr ────────┘不过这里要注意weak_ptr不是直接拥有Object它只是通过控制块观察Object是否仍然存在把三种智能指针放到一起就比较清楚了。unique_ptr 一个资源 ↓ 一个所有者 ↓ 不能拷贝 ↓ 可以moveshared_ptr 一个资源 ↓ 多个所有者 ↓ strong_count ↓ 强引用为0释放资源weak_ptr 不拥有资源 ↓ 不会增加strong_count ↓ 观察shared_ptr资源 ↓ lock获得临时shared_ptr ↓ 解决循环引用如果面试中被问weak_ptr是干什么的可以直接回答weak_ptr是对shared_ptr管理对象的一种弱引用它不会增加对象的强引用计数因此不会影响对象生命周期。它主要用于解决shared_ptr的循环引用问题。使用时一般通过lock()尝试获取一个临时shared_ptr如果对象已经销毁则获取失败。标准库中shared_ptr和weak_ptr共享同一个控制块控制块中会维护强引用计数和弱引用计数。这样unique_ptr ↓ shared_ptr ↓ weak_ptr三个智能指针从独占所有权到共享所有权再到非拥有型观察整个逻辑就完整串起来了。0voice · GitHub
返回列表