【转载】【C++】weak_ptr 弱引用智能指针详解

2,069 阅读5分钟

版权声明:本文为CSDN博主「Yngz_Miao」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:blog.csdn.net/qq_38410730…

weak_ptr 这个指针天生一副小弟的模样,也是在 C++11 的时候引入的标准库,它的出现完全是为了弥补它老大 shared_ptr 天生有缺陷的问题。

相比于上一代的智能指针 auto_ptr 来说,新进老大 shared_ptr 可以说近乎完美,但是通过引用计数实现的它,虽然解决了指针独占的问题,但也引来了引用成环的问题,这种问题靠它自己是没办法解决的,所以在 C++11 的时候将 shared_ptrweak_ptr 一起引入了标准库,用来解决循环引用的问题。

循环引用

什么是循环引用的问题呢?在 shared_ptr 的使用过程中,当强引用计数为 0 是,就会释放所指向的堆内存。那么问题来了,如果和死锁一样,当两个 shared_ptr 互相引用,那么它们就永远无法被释放了。

例如:

#include <iostream>
#include <memory>

class CB;
class CA {
  public:
    CA() {
      std::cout << "CA()" << std::endl;
    }
    ~CA() {
      std::cout << "~CA()" << std::endl;
    }
    void set_ptr(std::shared_ptr<CB>& ptr) {
      m_ptr_b = ptr;
    }
  private:
    std::shared_ptr<CB> m_ptr_b;
};

class CB {
  public:
    CB() {
      std::cout << "CB()" << std::endl;
    }
    ~CB() {
      std::cout << "~CB()" << std::endl;
    }
    void set_ptr(std::shared_ptr<CA>& ptr) {
      m_ptr_a = ptr;
    }
  private:
    std::shared_ptr<CA> m_ptr_a;
};

int main()
{
  std::shared_ptr<CA> ptr_a(new CA());
  std::shared_ptr<CB> ptr_b(new CB());
  ptr_a->set_ptr(ptr_b);
  ptr_b->set_ptr(ptr_a);
  std::cout << ptr_a.use_count() << " " << ptr_b.use_count() << std::endl;

  return 0;
}

编译并运行结果,打印为:

yngzmiao@yngzmiao-virtual-machine:~/test$ ./main 
CA()
CB()
2 2

对于打印的内容,你可能会觉得很奇怪,为什么析构函数并没有调用呢?

既然析构函数没有调用,就说明 ptr_aptr_b 两个变量的引用计数都不是 0。下面分析一下例子中的引用情况:

image.png

起初定义完 ptr_aptr_b 时,只有 ①、③ 两条引用,即 ptr_a 指向 CA 对象,ptr_b 指向 CB 对象。然后调用函数 set_ptr 后又增加了 ②、④ 两条引用,即 CB 对象中的 m_ptr_a 成员变量指向 CA 对象,CA 对象中的 m_ptr_b 成员变量指向 CB 对象。

这个时候,指向 CA 对象的有两个,指向 CB 对象的也有两个。当 main函数运行结束时,对象 ptr_aptr_b 被销毁,也就是 ①、③ 两条引用会被断开,但是 ②、④ 两条引用依然存在,每一个的引用计数都不为 0,结果就导致其指向的内部对象无法析构,造成内存泄漏。

weak_ptr

解决循环引用 weak_ptr 的出现就是为了解决 shared_ptr 的循环引用的问题的。以上文的例子来说,解决办法 就是将两个类中的一个成员变量改为 weak_ptr 对象,比如将 CB 中的成员变量改为 weak_ptr 对象,即 CB 类的代码如下:

class CB {
  public:
    CB() {
      std::cout << "CB()" << std::endl;
    }
    ~CB() {
      std::cout << "~CB()" << std::endl;
    }
    void set_ptr(std::shared_ptr<CA>& ptr) {
      m_ptr_a = ptr;
    }
  private:
    std::weak_ptr<CA> m_ptr_a;
};

编译并运行结果,打印为:

yngzmiao@yngzmiao-virtual-machine:~/test$ ./main 
CA()
CB()
1 2
~CA()
~CB()

通过这次结果可以看到,CACB 的对象都被正常的析构了。修改后例子中的引用关系如下图所示:

image.png

流程与上一例子大体相似,但是不同的是 这条引用是通过 weak_ptr 建立的,并不会增加引用计数。也就是说,CA 的对象只有一个引用计数,而 CB 的对象只有 2 个引用计数,当 main 函数返回时,对象 ptr_aptr_b 被销毁,也就是 ①、③ 两条引用会被断开,此时 CA 对象的引用计数会减为 0,对象被销毁,其内部的 m_ptr_b 成员变量也会被析构,导致 CB 对象的引用计数会减为 0,对象被销毁,进而解决了引用成环的问题。

如果仔细看代码的话,会觉得很神奇!定义 m_ptr_a 修改成 std::weak_ptr 类型,但是 set_ptr 函数定义的参数还是 std::shared_ptr 类型。这个时候为什么没有报错? weak_ptrshared_ptr 的联系是什么呢?

weak_ptr 的原理

weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它指向一个由 shared_ptr 管理的对象而不影响所指对象的生命周期,也就是,将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。不论是否有 weak_ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。从这个角度看,weak_ptr 更像是 shared_ptr 的一个助手而不是智能指针。

初始化方式

  • 通过 shared_ptr 直接初始化,也可以通过隐式转换来构造;
  • 允许移动构造,也允许拷贝构造。
#include <iostream>
#include <memory>

class Frame {};

int main()
{
  std::shared_ptr<Frame> f(new Frame());
  std::weak_ptr<Frame> f1(f);                     // shared_ptr直接构造
  std::weak_ptr<Frame> f2 = f;                    // 隐式转换
  std::weak_ptr<Frame> f3(f1);                    // 拷贝构造函数
  std::weak_ptr<Frame> f4 = f1;                   // 拷贝构造函数
  std::weak_ptr<Frame> f5;
  f5 = f;                                         // 拷贝赋值函数
  f5 = f2;                                        // 拷贝赋值函数
  std::cout << f.use_count() << std::endl;        // 1

  return 0;
}

需要注意weak_ptr 绑定到一个 shared_ptr 不会改变shared_ptr 的引用计数。

常用操作

  • w.user_count():返回 weak_ptr 的强引用计数;
  • w.reset(…):重置 weak_ptr

如何判断 weak_ptr 指向对象是否存在?

既然 weak_ptr 并不改变其所共享的 shared_ptr 实例的引用计数,那就可能存在 weak_ptr 指向的对象被释放掉这种情况。这时,就不能使用 weak_ptr 直接访问对象。那么如何判断 weak_ptr 指向对象是否存在呢?C++ 中提供了 lock 函数来实现该功能。如果对象存在,lock() 函数返回一个指向共享对象的shared_ptr (引用计数会增 1),否则返回一个空 shared_ptrweak_ptr 还提供了 expired() 函数来判断所指对象是否已经被销毁。

由于 weak_ptr 并没有重载 operator ->operator * 操作符,因此不可直接通过 weak_ptr 使用对象,同时也没有提供 get 函数直接获取裸指针。典型的用法是调用其 lock 函数来获得 shared_ptr 示例,进而访问原始对象。

使用场景

共享对象的 线程安全 问题

例如:线程 A 和线程 B 访问一个共享的对象,如果线程 A 正在析构这个对象的时候,线程 B 又要调用该共享对象的成员方法,此时可能线程 A 已经把对象析构完了,线程 B 再去访问该对象,就会发生不可预期的错误。

#include <iostream>
#include <memory>
#include <thread>

class Test {
  public:
    Test(int id) : m_id(id) {}
    void showID() {
      std::cout << m_id << std::endl;
    }
  private:
    int m_id;
};

void thread1(Test* t) {
  std::this_thread::sleep_for(std::chrono::seconds(2));
  t->showID();                      // 打印结果:0
}

int main()
{
  Test* t = new Test(2);
  std::thread t1(thread1, t);
  delete t;
  t1.join();

  return 0;
}

在例子中,由于 thread1 等待 2s,此时,main 线程早已经把 t 对象析构了。打印 m_id,自然不能打印出 2 了。可以通过 shared_ptrweak_ptr 来解决共享对象的线程安全问题。

#include <iostream>
#include <memory>
#include <thread>

class Test {
  public:
    Test(int id) : m_id(id) {}
    void showID() {
      std::cout << m_id << std::endl;
    }
  private:
    int m_id;
};

void thread2(std::weak_ptr<Test> t) {
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::shared_ptr<Test> sp = t.lock();
  if(sp)
    sp->showID();                      // 打印结果:2
}

int main()
{
  std::shared_ptr<Test> sp = std::make_shared<Test>(2);
  std::thread t2(thread2, sp);
  t2.join();

  return 0;
}

如果想访问对象的方法,先通过 tlock 方法进行提升操作,把 weak_ptr 提升为 shared_ptr 强智能指针。提升过程中,是通过检测它所观察的强智能指针保存的 Test 对象的引用计数,来判定 Test 对象是否存活。sp 如果为 nullptr,说明 Test 对象已经析构,不能再访问;如果 sp!=nullptr,则可以正常访问 Test 对象的方法。

如果设置 t2 为分离线程 t2.detach(),让 main 主线程结束,sp 智能指针析构,进而把 Test 对象析构,此时 showID 方法已经不会被调用,因为在 thread2 方法中, t 提升到 sp 时,lock 方法判定 Test 对象已经析构,提升失败!

观察者模式

观察者模式就是,当观察者观察到某事件发生时,需要通知监听者进行事件处理的一种设计模式。

在多数实现中,观察者通常都在另一个独立的线程中,这就涉及到在多线程环境中,共享对象的线程安全问题(解决方法就是使用上文的智能指针)。这是因为在找到监听者并让它处理事件时,其实在多线程环境中,肯定不明确此时监听者对象是否还存活,或是已经在其它线程中被析构了,此时再去通知这样的监听者,肯定是有问题的。

也就是说,当观察者运行在独立的线程中时,在通知监听者处理该事件时,应该先判断监听者对象是否存活,如果监听者对象已经析构,那么不用通知,并且需要从 map 表中删除这样的监听者对象。其中的主要代码为:

// 存储监听者注册的感兴趣的事件
unordered_map<int, list<weak_ptr<Listener>>> listenerMap;

// 观察者观察到事件发生,转发到对该事件感兴趣的监听者
void dispatchMessage(int msgid) {
  auto it = listenerMap.find(msgid);
  if (it != listenerMap.end()) {
    for (auto it1 = it->second.begin(); it1 != it->second.end(); ++it1) {
      shared_ptr<Listener> ps = it1->lock();            // 智能指针的提升操作,用来判断监听者对象是否存活
      if (ps != nullptr) {                              // 监听者对象如果存活,才通知处理事件
        ps->handleMessage(msgid);
      } else {
        it1 = it->second.erase(it1);                    // 监听者对象已经析构,从map中删除这样的监听者对象
      }
    }
  }
}

这个想法来源于:一个用 C++ 写的开源网络库,muduo 库,作者陈硕。大家可以在网上下载到 muduo 的源代码,该源码中对于智能指针的应用非常优秀,其中借助 shared_ptrweak_ptr 解决了这样一个问题,多线程访问共享对象的线程安全问题。

解决循环引用

循环引用,简单来说就是:两个对象互相使用一个 shared_ptr 成员变量指向对方的会造成循环引用,导致引用计数失效。上文详细讲述了循环引用的错误原因和解决办法。

监视 this 智能指针

在上文讲述 shared_ptr 的博文中就有讲述到: enable_shared_from_this 中有一个弱指针 weak_ptr,这个弱指针能够监视 this。在调用 shared_from_this 这个函数时,这个函数内部实际上是调用 weak_ptrlock 方法。lock() 会让 shared_ptr 指针计数 +1 ,同时返回这个 shared_ptr

相关阅读