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

2023-05-16

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_a和ptr_b两个变量的引用计数都不是0。下面分析一下例子中的引用情况:

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

这个时候,指向CA对象的有两个,指向CB对象的也有两个。当main函数运行结束时,对象ptr_a和ptr_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()

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

流程与上一例子大体相似,但是不同的是④这条引用是通过weak_ptr建立的,并不会增加引用计数。也就是说,CA的对象只有一个引用计数,而CB的对象只有2个引用计数,当main函数返回时,对象ptr_a和ptr_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的一个助手而不是智能指针

初始化方式

  1. 通过shared_ptr直接初始化,也可以通过隐式转换来构造
  2. 允许移动构造,也允许拷贝构造
#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_ptr。weak_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_ptr和weak_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;
}

如果想访问对象的方法,先通过t的lock方法进行提升操作,把weak_ptr提升为shared_ptr强智能指针。提升过程中,是通过检测它所观察的强智能指针保存的Test对象的引用计数,来判定Test对象是否存活。ps如果为nullptr,说明Test对象已经析构,不能再访问;如果ps!=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_ptr和weak_ptr解决了这样一个问题,多线程访问共享对象的线程安全问题。

解决循环引用

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

监视this智能指针

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


相关阅读

  • C++设计模式 - 观察者Observer模式
  • 第22课 weak_ptr弱引用智能指针
  • 智能指针(三):weak_ptr浅析
  • 深入掌握C++智能指针
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C++】weak_ptr弱引用智能指针详解 的相关文章

随机推荐

  • Error running ‘xxxx [clean]‘:Already disposed: Module: ‘xxxx‘

    今天将shardingsphere将版本5 3 0回滚到5 1 2版本 xff0c 然后我idea就莫名其妙的卡死了 xff0c 然后重启idea去clean maven操作 xff0c 就出现这个了 Error running 39 sh
  • kingbase迁移-bit字符串的长度(2)与bit类型(1)不匹配.

    今天做mysql转kingbase8 迁移完成后 xff0c 出现报错如下 Caused by com kingbase8 util KSQLException 错误 bit字符串的长度 2 与bit类型 1 不匹配 Where COPY
  • idea maven打包编译报错 java.lang.AssertionError: input.getType

    今天使用idea打包编译maven项目 xff0c 出现如下报错 构建报错时 xff0c 最先显示的是这个报错 查了一圈下来 xff0c 我的配置是没有问题的 Failed to execute goal org apache maven
  • gitee仓库人员上限如何一键删人

    我碰到情况是 xff0c 自己的gitee码云 xff0c 新建了很多仓库 xff0c 拉了谁都不知道 今天做了一个项目 xff0c 新建了仓库 xff0c 发了邀请链接 xff0c 想拉一下研发进仓库 xff0c 结果就提示人员上限了 我
  • docker启动pg出现报错Connection matched pg_hba.conf line 89: “local all postgres peer“

    今天在公司使用docker启动pg xff0c 查看日志出现以下报错 provided user name postgres and authenticated user name root do not match Peer authen
  • Deep Learning 最优化方法之Adam

    本文是Deep Learning 之 最优化方法系列文章的Adam方法 主要参考Deep Learning 一书 整个优化系列文章列表 xff1a Deep Learning 之 最优化方法 Deep Learning 最优化方法之SGD
  • 【CMake】CMakeLists.txt的超傻瓜手把手教程(附实例源码)

    新手写CMakeLists txt简直就是实力劝退 xff0c 各种命令让很多人头大 xff0c 如何写一个最基础的CMakeLists txt呢 xff1f 本文从一个实例出发 xff0c 教你编写的基本流程 CMakeLists txt
  • 【Gradle】Groovy的语法详解(下篇)

    上文介绍了Groovy的一般省略规则 xff0c 以及字符串 列表 Map的使用方式 xff0c 然而在Groovy中 xff0c 闭包是其中的一大杀器 xff0c 可以对代码的结构和逻辑带来的优化显而易见 本博文主要对Groovy闭包的使
  • 【OpenCV】OpenCV图像/视频的读取与写入(C++版)

    使用C 43 43 开发图像处理算法时 xff0c 最基础的就是利用OpenCV完成图像文件的输入 输出以及自动内存管理 重点 所以 xff0c 只要需要掌握一些简单的OpenCV的操作即可 本博文就对这些基础内容进行讲解 图像操作 图像读
  • 【OpenCV】OpenCV常用函数(C++版)

    俗话说 xff1a 好记性不如烂笔头 在使用OpenCV的过程中 xff0c 时常会用到很多函数 xff0c 而且往往可能会一时记不起这个函数的具体参数怎么设置 xff0c 故在此将常用函数做一汇总 图像缩放与放大 对图像的各项操作中 xf
  • 【Python】python曲线拟合

    python作为一款可以简单方便地进行科学计算的语言 xff0c 进行曲线拟合自然是必备的功能之一了 本文就如何进行曲线拟合进行讲解 本文需要进行拟合的数据为 xff1a x 61 np arange 1 31 1 y 61 np arra
  • 【C++】NULL和nullptr的关联与差别

    在写代码的过程中 xff0c 有时候需要将指针赋值为空指针 xff0c 以防止野指针 在C中 xff0c 都是使用NULL来实现的 xff1b 在C 43 43 中 xff0c 除了NULL之外 xff0c 还提供了nullptr来进行定义
  • 【C++】C++11可变参数模板(函数模板、类模板)

    在C 43 43 11之前 xff0c 类模板和函数模板只能含有固定数量的模板参数 C 43 43 11增强了模板功能 xff0c 允许模板定义中包含0到任意个模板参数 xff0c 这就是可变参数模板 可变参数模板的加入使得C 43 43
  • 【C++】C++11统一初始化(initializer_list<T>源码分析)

    C 43 43 11之前的初始化语法很乱 xff0c 有四种初始化方式 xff0c 而且每种之前甚至不能相互转换 让人有种剪不断 xff0c 理还乱的感觉 因此 xff0c C 43 43 11添加了统一初始化的方式 xff0c 本文将对统
  • 【C++】右值引用、移动语义、完美转发(上篇)

    在C 43 43 11 xff0c 引入了右值引用的概念 xff0c 在此基础上的移动语义在STL容器中使用非常广泛 简单来说 xff0c move语义使得你可以用廉价的move赋值替代昂贵的copy赋值 xff0c 完美转发使得可以将传来
  • 【C++】右值引用、移动语义、完美转发(下篇)

    上篇中 xff0c 主要讲解了右值引用和移动语义的具体定义和用法 在C 43 43 11中几乎所有的容器都实现了移动语义 xff0c 以方便性能优化 本文以C 43 43 11容器中的insert方法为例 xff0c 详细讲解在容器中移动语
  • AI==喜茶??

    2017年7月10日 xff0c 上海 xff0c 雨 刚从某CV方向的公司下班 xff0c 骑着小黄车朝着浦东某郊区租了一个月的床位行驶着 xff0c 雨打在脸上 xff0c 有点生疼 我不禁在思考 xff0c 这一切到底为了什么 xff
  • 【C++】unique_ptr独占型智能指针详解

    指针是C C 43 43 区别于其他语言的最强大的语法特性 xff0c 借助指针 xff0c C C 43 43 可以直接操纵内存内容 但是 xff0c 指针的引入也带来了一些使用上的困难 xff0c 这要求程序员自己必须手动地对分配申请的
  • 【C++】shared_ptr共享型智能指针详解

    指针是C C 43 43 区别于其他语言的最强大的语法特性 xff0c 借助指针 xff0c C C 43 43 可以直接操纵内存内容 但是 xff0c 指针的引入也带来了一些使用上的困难 xff0c 这要求程序员自己必须手动地对分配申请的
  • 【C++】weak_ptr弱引用智能指针详解

    weak ptr这个指针天生一副小弟的模样 xff0c 也是在C 43 43 11的时候引入的标准库 xff0c 它的出现完全是为了弥补它老大shared ptr天生有缺陷的问题 相比于上一代的智能指针auto ptr来说 xff0c 新进