【C++入门到精通】智能指针 shared_ptr循环引用 | weak_ptr 简介及C++模拟实现 [ C++入门 ]

news2024/9/28 13:27:11

在这里插入图片描述

阅读导航

  • 引言
  • 一、std::shared_ptr的循环引用
    • 1. 概念
    • 2. 示例分析
  • 二、std::weak_ptr
    • 1. 简介
    • 2. weak_ptr模板类提供的成员方法
    • 3. 使用示例
      • (1)weak_ptr指针的创建
      • (2)完整示例(解决上面循环引用问题)
    • 4. C++模拟实现
  • 温馨提示

引言

欢迎阅读本系列文章的第二篇,我们将继续探讨与 shared_ptr 相关的主题。上一篇文章我们介绍了 shared_ptr 的强大功能,但也提到了它可能面临的一个问题 —— 循环引用。当两个或多个对象之间相互持有 shared_ptr 的引用时,就会形成循环引用,导致这些对象无法被正确释放,从而引发内存泄漏。

在本文中,我们将深入讨论循环引用问题,并引入另一个智能指针类——weak_ptrweak_ptrshared_ptr 的伙伴,它可以帮助我们解决循环引用问题,并且不会增加引用计数,以避免对象无法释放的情况

通过学习 shared_ptrweak_ptr 的组合使用,我们将能够更好地管理动态分配的对象,避免内存泄漏,并提高代码的健壮性和可维护性。敬请期待本文的剖析和示例,希望能给您带来更深入的了解和实践经验。

一、std::shared_ptr的循环引用

1. 概念

当使用 std::shared_ptr 时,循环引用是一种常见的问题。循环引用指的是两个或多个对象彼此持有 shared_ptr 的引用,形成一个环状依赖关系。这种情况下,即使没有外部引用指向这些对象,它们的引用计数也无法降为零,从而导致内存泄漏

循环引用可能会导致内存泄漏的发生,因为每个对象都会持有对其他对象的引用,导致它们的引用计数无法归零。当没有外部引用指向这些对象时,它们的析构函数不会被调用,从而导致资源无法正确释放。

2. 示例分析

首先我们来看一段代码,这段代码就明显存在着循环引用。

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
	~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

循环引用分析

  1. node1node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete
  2. node1_next指向node2node2_prev指向node1,引用计数变成2。
  3. node1node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了
  5. 也就是说_prev析构了,node1就释放了
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放

⭕让我们通过下面这个图片来说上面这个问题:
在这里插入图片描述

为了解决循环引用问题,可以使用 std::weak_ptrstd::weak_ptr 是一种弱引用,它可以指向 std::shared_ptr 持有的对象,但不会增加对象的引用计数。这样,即使存在循环引用,通过使用 std::weak_ptr 可以打破循环引用,使对象的引用计数能够正确降为零,从而触发析构函数的调用。

二、std::weak_ptr

1. 简介

std::weak_ptr 是 C++11 标准库中提供的一种弱引用智能指针,它可以指向 std::shared_ptr 所管理的对象,但不会增加对象的引用计数。因此,当使用 std::weak_ptr 时,如果 std::shared_ptr 对象被释放或者过期,std::weak_ptr 将自动失效,避免了循环引用导致的内存泄漏问题

🔴std::weak_ptr官方文档

在这里插入图片描述

2. weak_ptr模板类提供的成员方法

shared_ptr<T>unique_ptr<T> 相比,weak_ptr<T> 模板类提供的成员方法不多,下表罗列了常用的成员方法及各自的功能。

成员方法功能
operator=重载 = 赋值运算符,使得 std::weak_ptr 指针可以直接被 std::weak_ptr 或者 std::shared_ptr 类型指针赋值。
swap(x)其中 x 表示一个同类型的 std::weak_ptr 类型指针,该函数可以互换两个同类型 std::weak_ptr 指针的内容。
reset()将当前 std::weak_ptr 指针置为空指针。
use_count()查看指向和当前 std::weak_ptr 指针相同的 std::shared_ptr 指针的数量。
expired()判断当前 std::weak_ptr 指针是否过期(指针为空,或者指向的堆内存已经被释放)。
lock()如果当前 std::weak_ptr 已经过期,则该函数会返回一个空的 std::shared_ptr 指针;反之,该函数返回一个和当前 std::weak_ptr 指向相同的 std::shared_ptr 指针。

🚨🚨注意:weak_ptr<T> 模板类没有重载 *-> 运算符,因此 weak_ptr 类型指针只能访问某一 shared_ptr 指针指向的堆内存空间,无法对其进行修改

3. 使用示例

(1)weak_ptr指针的创建

创建 std::weak_ptr 指针的方式和创建 std::shared_ptr 的方式类似。下面列举了三种常见的创建 std::weak_ptr 的方式:

  1. std::shared_ptr 创建

可以通过将 std::shared_ptr 赋值给 std::weak_ptr 来创建一个弱引用指针,例如:

std::shared_ptr<int> sptr = std::make_shared<int>(42);
std::weak_ptr<int> wptr(sptr);

上述代码中,我们首先创建了一个 std::shared_ptr 对象 sptr,它指向一个动态分配的 int 类型对象。然后,我们将 sptr 赋值给 std::weak_ptr 对象 wptr,创建了一个弱引用指针。

  1. std::shared_ptr 转换

可以通过 std::shared_ptrweak_ptr 成员函数,将 std::shared_ptr 转换为 std::weak_ptr,例如:

std::shared_ptr<int> sptr = std::make_shared<int>(42);
std::weak_ptr<int> wptr = sptr->weak_ptr();

上述代码中,我们首先创建了一个 std::shared_ptr 对象 sptr,它指向一个动态分配的 int 类型对象。然后,我们调用 sptrweak_ptr() 成员函数,将 sptr 转换为 std::weak_ptr 对象 wptr,创建了一个弱引用指针。

  1. 使用 std::weak_ptr 的构造函数

可以直接使用 std::weak_ptr 的构造函数,创建一个空的弱引用指针,例如:

std::weak_ptr<int> wptr;

上述代码中,我们直接创建了一个空的 std::weak_ptr 对象 wptr,它不持有任何对象的引用。

(2)完整示例(解决上面循环引用问题)

使用 std::weak_ptr 修改上面的代码,可以将 _prev_next 成员变量改为 std::weak_ptr<ListNode> 类型。这样可以避免循环引用,同时仍然可以访问链表中的前一个节点和后一个节点

struct ListNode
{
	int _data;
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	~ListNode() { cout << "~ListNode()" << endl; }
};

int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;

	// 使用 weak_ptr.lock() 获取 shared_ptr 对象
	shared_ptr<ListNode> node1Next = node1->_next.lock();
	shared_ptr<ListNode> node2Prev = node2->_prev.lock();
	if (node1Next)
		cout << "node1 next data: " << node1Next->_data << endl;
	else
		cout << "node1 next is nullptr" << endl;
	if (node2Prev)
		cout << "node2 prev data: " << node2Prev->_data << endl;
	else
		cout << "node2 prev is nullptr" << endl;

	return 0;
}

运行上述代码,可以得到如下输出:

1
1
2
2
node1 next data: 0
node2 prev data: 0

在上面的代码示例中,我们创建了两个节点 node1node2,并通过 std::weak_ptr 进行相互引用。

shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);

接下来,我们设置节点之间的关系。修改前的代码如下:

node1->_next = node2;
node2->_prev = node1;

我们将 _next_prev 成员变量的类型从 shared_ptr<ListNode> 改为 weak_ptr<ListNode>

weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;

这样就建立了节点之间的弱引用关系,避免了循环引用。

接下来,我们可以通过 lock() 方法将 std::weak_ptr 转换为 std::shared_ptr,以访问所管理的对象。

shared_ptr<ListNode> node1Next = node1->_next.lock();
shared_ptr<ListNode> node2Prev = node2->_prev.lock();

如果 std::weak_ptr 不过期(即所管理的对象还存在),lock() 方法会返回一个有效的 std::shared_ptr 对象,否则返回空指针。

最后,我们可以使用这些 std::shared_ptr 对象来访问链表中的前驱和后继节点的数据。

if (node1Next)
    cout << "node1 next data: " << node1Next->_data << endl;
else
    cout << "node1 next is nullptr" << endl;

if (node2Prev)
    cout << "node2 prev data: " << node2Prev->_data << endl;
else
    cout << "node2 prev is nullptr" << endl;

通过这种方式,我们可以安全地访问链表中的前驱和后继节点,而不会导致循环引用和内存泄漏。

4. C++模拟实现

template<class T>
class weak_ptr
{
public:
    // 默认构造函数,将_ptr成员指针初始化为nullptr
    weak_ptr()
        :_ptr(nullptr)
    {}

    // 接受shared_ptr参数的构造函数,将_ptr成员指针初始化为shared_ptr所管理对象的指针
    weak_ptr(const shared_ptr<T>& sp)
        :_ptr(sp.get())
    {}

    // 重载*运算符,返回所管理对象的引用
    T& operator*()
    {
        return *_ptr;
    }

    // 重载->运算符,返回所管理对象的指针
    T* operator->()
    {
        return _ptr;
    }

    // 返回所管理对象的指针
    T* get()
    {
        return _ptr;
    }

private:
    T* _ptr; // 所管理对象的指针
};

这段代码是一个简化版的 weak_ptr 类的实现,提供了一些基本的功能。

首先,我们可以看到 weak_ptr 类有一个默认构造函数和一个接受 shared_ptr 参数的构造函数。默认构造函数将 _ptr 成员指针初始化为 nullptr,而接受 shared_ptr 参数的构造函数将 _ptr 成员指针初始化为 shared_ptr 所管理对象的指针。

接下来,我们可以看到 weak_ptr 重载了 *-> 运算符,使得可以像使用指针一样,通过 weak_ptr 对象访问所管理的对象。operator*() 返回所管理对象的引用,operator->() 返回所管理对象的指针。

同时,weak_ptr 还提供了一个 get() 方法,返回 _ptr 指针,即所管理对象的指针。这允许用户直接访问 _ptr 指针,但需要注意,这种直接访问可能会导致悬空指针问题,因为 _ptr 指针可能已经无效(所管理对象已被释放)。

🚨🚨注意:这个简化版的 weak_ptr 实现没有考虑线程安全性。在实际应用中,weak_ptr 需要与其他智能指针共同使用,比如 shared_ptrunique_ptr,并且需要考虑线程安全性和异常安全性

温馨提示

感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1409932.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

幻兽帕鲁PalWorld服务器搭建教程,1分钟开服,纯小白教程,无需基础

雨云面板服快速开幻兽帕鲁PalWorld服务器的教程&#xff0c;配置文件修改方法和配置项中文注释。 最近这游戏挺火&#xff0c;很多人想跟朋友联机&#xff0c;如果有专用服务器&#xff0c;就不需要房主一直开着电脑&#xff0c;稳定性也好得多。 幻兽帕鲁简介 《幻兽帕鲁》…

Revit二次开发 设置材质

设置此处材质&#xff0c;需要在材质浏览器中创建材质&#xff0c;根据材质名字设置此材质。 代码如下&#xff1a; Material material new FilteredElementCollector(doc).OfClass(typeof(Material)).FirstOrDefault(x > x.Name "窗框") as Material; Element…

掼蛋的文化价值

近几年&#xff0c;掼蛋成为人们在各种场合休闲娱乐时的首选&#xff01;掼蛋的魅力在于它不仅是一款简单有趣的游戏&#xff0c;更是中国传统文化和智慧的缩影&#xff01;它所积淀的文化价值、所蕴含的文化元素、所散发的文化气息具体包括&#xff1a; 1.社交文化&#xff1a…

android camera的使用以及输出的图像格式

一、Camera 1.1、结合SurfaceView实现预览 1.1.1、布局 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:app"http://schemas.android.com/apk/res-au…

3d导模型赋予材质方法---模大狮模型网

给3D模型赋予材质的方法可以根据您使用的软件和工作流程而有所不同。以下是一般的步骤&#xff0c;您可以根据自己的情况进行调整&#xff1a; 准备模型&#xff1a;首先&#xff0c;确保您的模型已经完全建模并进行了UV映射。UV映射是将2D纹理坐标应用到3D模型表面的过程&…

对 MODNet 网络结构直接剪枝的探索

文章目录 1 写在前面2 遇到问题3 解决方案4 探索过程4.1 方案一4.2 方案二4.3 方案三 5 疑惑与思考5.1 Q15.2 Q2 1 写在前面 在前面的文章中&#xff0c;笔者与小伙伴们分享了对 MODNet 主干网络部分以及其余分支分别剪枝的探索历程&#xff0c;即先分解、再处理、后融合的手法…

DB2数据库,时间类型插入数据

DB2数据库,时间类型插入数据 1、TIMESTAMP类型 1.1、创建表 CREATE TABLE BI_varchar ( id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1 ), hide_zero varchar(1000), simulation_cphone_de varchar(1000), disorder_de varchar(…

Cortex-M4 处理器 内存模型

内存模型 处理器有一个固定的默认内存映射&#xff0c;提供最多4GB的可寻址内存。 SRAM和外设的区域包括可选的位带区域。 位带提供了对位数据的原子操作 处理器为核心外设寄存器保留专用外设总线&#xff08;PPB&#xff09;地址范围的区域。 内存区域、类型和属性 内存映…

3D打印机 拓竹A1 Combo 开箱体验

拓竹&#xff08;Bambu Lab&#xff09;A1 Combo FDM 3D打印机开箱体验。 最近想玩玩3D打印&#xff0c;所以入手了一台拓竹A1 Combo的3D打印机&#xff0c;A1 Combo对比A1多了个AMS lite&#xff0c;支持多色打印&#xff08;4种颜色&#xff09;&#xff0c;京东买的&#x…

Redis——关于它为什么快?使用场景?以及使用方式?为何引入多线程?

目录 1.既然redis那么快&#xff0c;为什么不用它做主数据库&#xff0c;只用它做缓存&#xff1f; 2.Redis 一般在什么场合下使用&#xff1f; 3.redis为什么这么快&#xff1f; 4.Redis为什么要引入了多线程&#xff1f; 1.既然redis那么快&#xff0c;为什么不用它做主数据…

在线SM4加密/解密工具

在线SM4加密/解密 - BTool在线工具软件&#xff0c;为开发者提供方便。在线SM4加密/解密工具支持快速、便捷地对数据进行SM4算法加密与解密。适用于各类业务场景&#xff0c;确保信息安全传输&#xff0c;操作简易直观&#xff0c;只需几步即可完成加解密过程。采用国家标准SM4…

Webpack5 基本使用 - 1

Webpack 是什么 webpack 的核心目的是打包&#xff0c;即把源代码一个一个的 js 文件&#xff0c;打包汇总为一个总文件 bundle.js。 基本配置包括mode指定打包模式&#xff0c;entry指定打包入口&#xff0c;output指定打包输出目录。 另外&#xff0c;由于 webpack默认只能打…

Python工具:pathlib

文件的路径实际上是一件很困扰的时间&#xff08;各种平台有时候规则不一样&#xff0c;有时候还需要考虑字符转义的问题&#xff09;&#xff0c;因此我直接推荐使用模块 pathlib&#xff0c;当然&#xff0c;如果您不介意的话&#xff0c;可以使用 os.path 做较为低级的路径操…

蓝桥杯(Python)每日练Day5

题目 OJ1229 题目分析 题目完全符合栈的特征&#xff0c;后进先出。如果能够熟练使用列表的9种方法那么这道题很容易解出。 题解 a[]#存衣服 nint(input()) for i in range(n):llist(input().split())#判断每一步的操作if len(l[0])2:a.append(l[1])else:while a.pop()!l…

不停机迁移,TDengine 在 3D 打印技术中的“焕新”之路

小T导读&#xff1a;自 2021 年我们正式使用 TDengine 至今已接近三年&#xff0c;现在 TDengine 已经成熟应用于我们多个项目当中&#xff0c;凭借着强大的读写存储能力&#xff0c;为我司多项业务的核心数据保驾护航。近期我们团队刚好完成 TDengine 2.x 到 3.x 的数据迁移&a…

Redisson 分布式锁可重入的原理

目录 1. 使用 Redis 实现分布式锁存在的问题 2. Redisson 的分布式锁解决不可重入问题的原理 1. 使用 Redis 实现分布式锁存在的问题 不可重入&#xff1a;同一个线程无法两次 / 多次获取锁举例 method1 执行需要获取锁method2 执行也需要&#xff08;同一把&#xff09;锁如…

Redis面试

1.说说什么事redis Redis是一种基于键值对的NoSql数据库。 Redis中的value支持string&#xff08;字符串&#xff09;、hahs&#xff08;哈希&#xff09;、list、set、zset&#xff08;有序集合&#xff09;、bitmaps&#xff08;位图&#xff09;&#xff0c;HyperLoglog等数…

树的学习day01

树的理解 树是一种递归形式的调用 树是由于多个结点组成的有限集合T 树中有且仅有一个结点称为根 当结点大于1的时候&#xff0c;往往其余的结点为m个互不相交的有限个集合T1,…,Tm&#xff0c;每个互不相交的有限集合本身右是一棵树&#xff0c;称为这个根的子树 空树也是树 关…

【Web前端开发基础】CSS3之空间转换和动画

CSS3之空间转换和动画 目录 CSS3之空间转换和动画一、空间转换1.1 概述1.2 3D转换常用的属性1.3 3D转换&#xff1a;translate3d&#xff08;位移&#xff09;1.4 3D转换&#xff1a;perspective&#xff08;视角&#xff09;1.5 3D转换&#xff1a;rotate3d&#xff08;旋转&a…

城市开发区视频系统建设方案:打造视频基座、加强图像数据治理

一、背景需求 随着城市建设的步伐日益加快&#xff0c;开发区已经成为了我国工业化、城镇化和对外开放的重要载体。自贸区、开发区和产业园的管理工作自然也变得至关重要。在城市经开区的展览展示馆、进出口商品展示交易中心等地&#xff0c;数千路监控摄像头遍布各角落&#…