【C++】简单学——list类

模拟实现之前需要了解的

概念

带头双向链表(double-linked),允许在任何位置进行插入

区别

相比vector和string,多了这个

已经没有下标+[ ]了,因为迭代器其实才是主流

(要包头文件<list>)

方法

构造

和vector的构造差不多,无参、n个、迭代器、拷贝构造

迭代器失效

因为底层结构不一样

在list中的迭代器不会因为insert的挪动和扩容而失效,erase删除到自己就会失效

clear会把所有节点清楚,但是不清楚哨兵位

特殊的方法Operations

逆置reverse
排序sort

(默认升序,如果要改就要传仿函数)

list的排序意义不大,因为这个排序的效率太低

甚至vector还进行了两次拷贝构造,都比list高了差不多四倍的效率

list底层用的是归并排序,访问效率太低导致的,并不是算法本身效率低

合并merge

本质上就是取小的尾插

去重unique

但是要求先排序

remove

给一个值,然后进行删除(和erase不同,相当于是find加erase,find到了就删,没find到就不删)

remove_if

条件删除,和仿函数有关

splice

名字没取好,应该叫转移

作用:把某些值转移到某个位置去

pos就是位置之前,一个链表、某个链表的一个位置、某个链表的一部分

也可以自己转移自己

用法2

不删除节点,只会改变指向,把1转移到哨兵位之前,也就是4后面

用法1

把前面的那个链表整个转移到另个一链表的某个位置

另一个链表因为被转移了,所以list1就没节点了

转移的前提是类型必须匹配

链表的内部实现

1.先看源码

节点

链表的成员变量

link_type其实就是list_node*;成员就是一个节点指针

2.看构造函数

总结:调用了一个空初始化,然后这个空初始化建了一个哨兵位头结点(get_node()),然后next和prev都指向自己

以下内容看看就好

空间配置器调用一个allocate的接口来获取空间(单纯的获取节点)

创建一个节点,顺带初始化

3.看push_back

在哨兵位(end)的头结点那里插入,在哨兵位的prev插入,就是尾插

insert和我们自己搞的差不多,就不用多看了

主要是迭代器比较麻烦

正式开始模拟实现

非常需要画图

先搭个架子出来

list需要节点,所以需要再建一个节点类出来,节点用的是struct(库里面也用的是struct,只要是是需要大量公开的内容都可以用struct)

 链表的成员:

节点

成员变量

  1. 下个节点位置的指针
  2. 上个节点位置的指针
  3. 存值的地方

成员方法

  • 构造函数(在链表的构造函数地方,逻辑是先搞一个哨兵位头结点,然后指针全都指向自己,而在搞一个哨兵位头结点的时候会用到new Node,此时new Node就会去调用节点类的构造函数,所以我们需要提前把构造函数准备好)

链表

成员变量

  1. 一个哨兵位的头结点

构造函数

就先搞一个哨兵位头结点

pushback

暂且不考虑insert复用,因为要用到迭代器

尾插逻辑:

新建一个节点,然后让节点之间连接上:尾插就是在head的prev

步骤

  1. 先新建一个节点
  2. 记录没插之前的tail
  3. 然后修改tail和newnode和head之间的指向

因为有哨兵位,所以就算没有节点,当他们进行尾插的时候也不用考虑自己是第一个节点要怎么操作,这个是这个设计的优势(主要工作就是找到tail和head,因为有哨兵位,所以哪怕没有节点,tail也是head->prev=自己;剩下的就是改变指向了)

现在先实现一下遍历数据(前提就是有迭代器)

我们的目的是实现以下的遍历功能

  1. list<int>::iterator it = lt.begin();
  2. lt.end()
  3. *it
  4. ++it

这四个功能对于list迭代器来说:

list的迭代器不能用原生指针

因为节点空间不连续(没办法直接typedef T* iterator,因为list的T是节点,只用节点指针没办法搞定

  1. list<int>::iterator it = lt.begin();(这个倒是可以用这个访问方式)
  2. lt.end()(这个也可以解决)
  3. *it(对节点指针解引用得到的是节点,而不是节点的值,不满足)
  4. ++it(++it实际达到的效果是it指针在地址上往前移动一个节点size单位的空间,并不会到下一个节点处,不满足

原因:List是一个节点一个节点开辟空间的,所以空间一定是不连续的

vector可以的原因

  1. 每个节点之间都是连续的,而且T就是数据的类型,每次++的时候都是往前走T类型size的空间,刚好就可以走到下一个节点处
  2. 解引用之后就可以得到T的值,即自己想要的数据

需要解决的问题:

  1. *it(对节点指针解引用得到的是节点,而不是节点的值,不满足)
  2. ++it(++it实际达到的效果是it指针在地址上往前移动一个节点size单位的空间,并不会到下一个节点处,不满足

类比:

Node*原生指针不能满足我们的需求

我们的那个日期类,如果想要做到++或者日期减日期,这种操作原本都是不能直接用的,但是我们内部可以自己去控制(凭借运算符重载)

因此,如果我们没办法用原生指针去实现*it和++it,那我们就自己去封装一个迭代器的类(即内置类型不能解决我们的问题,那我们就实现成自定义类型),然后就写(typedef 迭代器的类 iterator;),这样就可以照常使用了

这个迭代器的类名字不重要,毕竟之后都是要用来typedef的

库里面也是这样实现的

可以看到,库里面的那个迭代器类的成员变量用的也是原生节点指针(link_type就是list_node*),在list里的迭代器也是原生节点指针,只不过因为原生节点指针的功能不符合我们的预期,所以我们就改造一下,用一个类去封装,然后去把这个原生节点指针的运算符给重载成我们需要的功能,就可以去掌控迭代器的功能了

也就是可以做到以下功能

  1. *it(返回节点的值,调用operator*)
  2. ++it(跳转到下一个节点,调用operator++
  3. (还有一个没有说的,因为迭代器类的成员变量就是一个节点指针,所以当sizeof这个迭代器的时候,得到的迭代器大小就是4,而这个4恰好就是自己那个节点指针的大小)

(类型决定了我们的行为是什么,如果是单单的原生指针,那++就是到下一个节点大小的位置处;如果说我们封装了这个原生指针,修改了这个类型的具体行为,那++我们就可以修改为到下一个节点)

实现迭代器类

迭代器的行为是统一的,目的是不关注底层实现,而是要关注用指针类型的来访问和修改元素

我们的迭代器类是实现成struct的,因为我们要让外面的人能直接用

成员变量:节点指针

成员方法:构造函数、operator++()、后置++:operator++(int)、

构造函数

使用场景:List<int>::iterator it = lt.begin();

这个it就是在用 lt.begin()的返回值Node* 来进行初始化

前置++和后置++(--也类似)

前置++返回的是++后的自己,后置++返回的是++前的自己

这个地方另外再typedef ListIterator<T> Self是因为后置++要返回的是原本的自己,也就是ListIterator<T> it或者说是*this

(这个地方会有点绕,因为this就是自己的指针,而中的自己则是ListIterator<T> it,所以需要解引用之后再返回)

 为了写的方便,所以我们把ListIterator<T>给typedef成了Self

所以:就是ListIterator<T> tmp(*this)

我们通过重载++修改了++的规则,让++可以正确地指向下一个节点

拷贝构造

在这个地方我们可以不用写拷贝构造,可以直接让编译器给我们自己生成那个浅拷贝的拷贝构造,因为我们的成员变量是一个节点的指针,浅拷贝这个节点的指针目的就是让那个拷贝构造出来的迭代器也指向那个节点,要的就是浅拷贝(并非成员变量中有指针就要深拷贝,要看具体情况)

用it拷贝构造出it2,it2也同样指向节点值为1的那个节点

析构函数

不需要写析构,析构的目的是为了释放资源,而迭代器指向的那块空间并不是属于迭代器的,而是属于list的,迭代器的作用仅仅是访问或者修改值,不需要析构

解引用

解引用要实现读和写的功能

运算符

operator!=和==

问:需不需要重载比较大小

要看有没有意义

答:不需要,因为没有意义,如果说是比较节点所在的位置的大小的话还好,但是这个是比较迭代器的大小,每个节点在new的时候地址都是无规则的,根本就不能保证后面的节点一定就比前面的节点的地址要大

正式开始链表类

目前已经搞好了迭代器类

现在我们要做的就是把这个迭代器和我的list连接起来

总结一下前面的部分:

 我们做的链表类是一个带头双向链表

我们的节点是一个个数据单元,我们的链表就是要把他们链接起来

链表的迭代器因为原生节点指针无法满足我们的功能,所以我们用一个类去封装了一下节点指针,再用运算符重载来掌控他的行为

还是要重申一下结构:这个链表类需要和迭代器类和节点类结合起来使用,他们之前的关系是并列的,其中,节点类和迭代器类使用的是struct,设置成struct就是为了让list可以访问,是专门为list类服务

如果说我们要访问我们的链表,那我们就要从第一个节点(_head->next)开始

begin()和end()

因为只有list类才知道链表的开头是在哪里,所以我们的begin要写在链表类里

上面的写法和下面的写法一样,没什么区别,只不过上面用了匿名对象

甚至可以这样写:走隐式类型转换,

有了这个我们就可以实现前面没有实现的遍历了

insert

问:为什么还要用cur记录pos,而不是直接用pos,不都是指向同一块空间吗?迭代器不就是指针吗?为什么还要取_node出来

答:迭代器是一个封装了节点指针的类,类里面放了节点指针,也就是_node,现在把他拿出来才可以进行找prev和next的操作,因为只有ListNode才有prev和next,迭代器只是ListNode*的一个盒子

erase

别忘了改变指向后还要delete

delete的是cur,而不是pos,我们不是delete外面的那个盒子,而是那个节点

pos已经被erase了

这个地方要小心迭代器失效的问题

所以我们要返回next(利用next构造一个next迭代器回去)

push_front和push_back

pop_back和pop_front

这个地方用--不要用-1,因为我们只提供了operator--没有提供operator-

然后我们就可以用范围for了

(因为底层被替换成迭代器了)

size

两种方案

  1. 每一次调用size()的时候都遍历一遍(库里是这样实现的)
  2. 加一个成员变量size_t size;每次创建节点的时候都size++,减少的时候就--(这个地方不是用静态成员变量,因为每个链表的长度都不一样,静态成员变量倒是可以记录一共创建了多少个链表,但绝对不是这里)(我们实现这个)

  • 成员变量加上_size
  • size方法直接返回成员变量_size
  • 初始化的时候赋值0 
  • 每次插入的时候都++
  • 每次删除的时候都--

empty

因为我们实现了_size,所以我们就直接看_size就好

也可以用_head来用

resize

这个我们就不写了,因为链表没有容量的概念,所以基本上就是n比size大,就往后尾插一堆,n比size小,就尾删一堆

假如说我们的链表要存储自定义类型

匿名对象

有名对象

多参数构造的隐式类型转换

C++11特有的:{}多参数的构造函数也支持隐式类型转换了,用花括号(这个地方不是initailizer_list类型)

但是这样子没办法遍历

这里是说<<不支持A类型使用

因为我们的A类型里面没有写

但如果对方不想写operator<<怎么解决

如果对方是公有的,我们就自己拿

但是这个地方怎么看怎么别扭,因为我们的迭代器的目标是模拟当前类型的指针,既然是指针,肯定就应该能像下面这样直接用箭头访问

所以我们还可以重载一个operator->

operator->

返回的是那个T类型的值的指针

这个地方会很诡异

如果用替换的方法的话,替换出来应该是A*a1,问题是A*是怎么直接啥都不用就可以访问a1的呢?

答:这个地方隐藏了一个箭头,编译器为了可读性省略了一个箭头

也可以不省略写,都一样

我们要把it当成A*来设计,迭代器就是模拟的T*

外面的箱子iterator里面放了一个小箱子A,从小箱子里面取一个物品_a1

我们用迭代器的时候就是希望直接通过->取_a1,所以operator->和operator*都是要跨两层的

const迭代器

如果直接在原本的begin或者end那个前面加上const,是没办法解决问题的

如果只是读的话那还好,因为非const的成员用const迭代器是权限的缩小,所以也可以用

我是一个const的链表,但是你通过非const的迭代器修改了我的值

罪魁祸首就是那个加了const的begin和end,原本迭代器是没办法去接收const成员的迭代器的,因为const成员不给你begin你就没辙

但是加了const之后,const成员也可以给你begin了,而你却只是一个普通迭代器而不是const迭代器,所以普通迭代器肯定可以修改

(const链表调用之后返回了普通迭代器)

因此我们需要增加一个可以返回const迭代器的重载版本,然后普通的链表就会调用普通迭代器,const的链表就会调用const迭代器

问:为什么不能写成这样?

答:上面写的const iterator是指本身这个iterator不能被修改

而const_iterator是指迭代器指向的内容不能被修改;

况且,iterator是一个类,如果说用const了这个类,就代表这他连++都调用不了了,因为类的那些成员不能被修改

所以我们得自己实现一个const_iterator类,让迭代器本身可以修改,但是指向的内容不能修改

我们需要解决的问题

  1. *it += 10不能操作
  2. clt.begin()返回的是const_iterator

这个地方可以干脆直接把原本的iterator给拷贝一下,稍微修改一些功能,让前面的两点功能能够实现

const_iterator就是一个普通 iterator的一个cv版本,只是修改一下名字,以及处理一下写方面的东西

就是因为cv版本,所以两个感觉有些冗余了,所以接下来的目的就是解决这个冗余

解决冗余(加模板)

就这两个返回值不同

想办法合并,用模板,用一个模板参数来控制

模板参数传引用、传指针

在这个地方,只要传一个T过去就可以了,另外两个会根据T而改变

本质上我们写了一个类模板

编译器实例化生成了两个类

  • 如果说我们搞的是const的链表,那我们在使用迭代器的时候就要用const_iterator,然后const_iterator会根据你的T类型,编译器生成各种const T& 和const T*的迭代器,然后当我们进行*it+=10的时候,会因为*it 返回的是const的T&而报错
  • 如果说我们搞的是普通链表,那我们在使用迭代器的时候就要用iterator,然后iterator会根据你的T类型,编译器生成各种T& 和T*的迭代器,然后当我们进行*it+=10的时候,就可以修改了

小小收个尾

还没有销毁链表

~list和clear

原本因为erase搞了之后,会迭代器失效,但是因为我们的erase返回了下一个迭代器,所以就可以这样

其实这个应该叫clear

但是这个it是到end就停下来,也就是说头结点没有被释放

所以说~list应该这样写

拷贝构造

默认的拷贝构造是一个浅拷贝,其中一个析构,另一个就全是野指针了

所以我们要自己实现

我们可以增加一个empty_init(其实就是把无参构造的内容给单独拿出来了)

这样会比较方便拷贝构造(因为在pushback的前提是有个哨兵位头结点)

拷贝逻辑:遍历另一个链表,然后全部插入

T有可能是string,所以auto要加上引用

链表的赋值

顺便也把swap给搞了

swap逻辑:交换成员

技巧:

因为需要析构,就是申请了资源的,有资源的肯定就是指针指向那个资源,然后就需要深拷贝的了

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

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

相关文章

Qt 国际化与本地化完整解决方案

在全球化的今天&#xff0c;软件支持多语言和本地化&#xff08;Internationalization & Localization&#xff0c;简称i18n & l10n&#xff09;已成为基本需求。Qt提供了一套完整的解决方案&#xff0c;帮助开发者轻松实现应用程序的国际化支持。本文将从原理到实践&a…

MNIST 手写数字识别模型分析

功能概述 这段代码实现了一个基于TensorFlow和Keras的MNIST手写数字识别模型。主要功能包括&#xff1a; 加载并预处理MNIST数据集构建一个简单的全连接神经网络模型训练模型并评估其性能使用训练好的模型进行预测保存和加载模型 代码解析 1. 导入必要的库 import matplot…

进阶系统策略

该策略主要基于价格动态分析,结合多种技术指标和数学计算来生成交易信号。其核心逻辑包括: 1. 价格极值计算:首先,策略计算给定周期(由`Var3`定义)内的最高价和最低价,分别存储在`Var12`和`Var13`中。这一步骤旨在捕捉价格的短期波动范围。 2. 相对位置计算:接着,策…

【Linux内核】Linux驱动开发

推荐书籍&#xff1a; 《Linux内核探秘&#xff1a;深入解析文件系统和设备驱动的架构与设计》 知识点 x86的IO地址空间和内存地址空间是独立的两套地址空间&#xff0c;并且使用不同的指令访问。MOV, IN, OUT。内存映射I/O可以将IO映射到内存。ARM等RISC采用统一编编址&#x…

MySQL用户管理(15)

文章目录前言一、用户用户信息创建用户修改密码删除用户二、数据库的权限MySQL中的权限给用户授权回收权限总结前言 其实与 Linux 操作系统类似&#xff0c;MySQL 中也有 超级用户 和 普通用户 之分 如果一个用户只需要访问 MySQL 中的某一个数据库&#xff0c;甚至数据库中的某…

react19相关问题和解答

目录 1. react19将ref放在了props中(不再需要 forwardRef),那么是不是可以通过ref获取子组件的全部变量了? 我的子组件的useImperativeHandle还需要定义吗? 1.1. ref 在 props 中的本质变化 1.2. 为什么不能访问全部变量? 2. In HTML,cannot be a descendant of. Thi…

Code Composer Studio:CCS 设置代码折叠

Code Composer Studio&#xff1a;设置代码折叠,可以按函数&#xff0c;if, 等把代码折叠起来。1.2.开启折叠选项3.开启后&#xff0c;如果文件已经打开&#xff0c;要关掉重新打开文件就可以开到折叠功能生效。

JMeter groovy 编译成.jar 文件

groovy 编译 一、windows 下手动安装Groovy 下载 Groovy 二进制包 前往官网&#xff1a;https://groovy.apache.org/download.html 下载 Binary release&#xff08; https://groovy.jfrog.io/ui/native/dist-release-local/groovy-zips/apache-groovy-sdk-4.0.27.zip &#xf…

使用maven-shade-plugin解决依赖版本冲突

项目里引入多个版本依赖时&#xff0c;最后只会使用其中一个&#xff0c;一般可以通过排除不使用的依赖处理&#xff0c;但是如果需要同时使用多个版本&#xff0c;可以使用maven-shade-plugin解决。以最典型的poi为例&#xff0c;poi版本兼容性很低&#xff0c;如果出现找不到…

[CH582M入门第十一步]DS18B20驱动

学习目标: 1、介绍DS18B20 2、学习单总线 3、学习DS18B20程序驱动一、DS18B20介绍 DS18B20 是一款由 Maxim Integrated(原Dallas Semiconductor) 推出的 数字温度传感器,以其单总线(1-Wire)通信协议、高精度和广泛应用而闻名。以下是其核心特点和应用介绍: 主要特性 数…

SGLang + 分布式推理部署DeepSeek671B满血版

部署设备&#xff1a;28A100 80G&#xff0c;两台机器&#xff0c;每台机器8张A100。 模型&#xff1a;deepseek-671B-int8 模型下载地址&#xff1a;https://huggingface.co/meituan/DeepSeek-R1-Block-INT8 模型参考&#xff1a; 1、SGLang Docker部署 github地址&#…

PCL 间接平差拟合球

目录 一、算法原理 1、计算流程 2、参考文献 二、代码实现 三、结果展示 本文由CSDN点云侠原创,首发于2025年7月24日。博客长期更新,本文最新更新时间为:2025年7月24日。 一、算法原理 1、计算流程 空间球方程: ( x − a ) 2 + ( y − b ) 2 + ( z − c ) 2 = R 2 (1) (…

基于 HAProxy 搭建 EMQ X 集群

负载均衡器&#xff08;LB&#xff09;负责分发设备的 MQTT 连接与消息到 EMQ X 集群&#xff0c;采用 LB 可以提高 EMQ X 集群可用性、实现负载平衡以及动态扩容。 HAProxy简介 HAProxy 是一款高性能的 开源负载均衡器 和 反向代理服务器&#xff0c;主要用于在多个服务器之…

RISC-V基金会Datacenter SIG月会圆满举办,探讨RAS、PMU性能分析实践和经验

一直以来&#xff0c;龙蜥社区在 RISC-V 生态建设中持续投入&#xff0c;并积极贡献上游社区。多位龙蜥社区成员在 RISC-V 国际基金会担任主席/副主席角色&#xff0c;与来自阿里云、阿里达摩院、中兴通讯、浪潮信息、中科院软件所、字节跳动、Google、 MIT、Akeana 等企业的专…

CloudComPy使用PyInstaller打包后报错解决方案

情况描述 笔者在spec文件中&#xff0c;datas变量设置如下。如果你的报错类似于“找不到cloudComPy”&#xff0c;先尝试如下的设置。 datas[(CloudCompare,cloudComPy)], 笔者在打包完成后&#xff0c;打开软件发现报错&#xff1a; from cloudComPy import* ModuleNotFoun…

node.js中的path模块

在 Node.js 中&#xff0c;path 模块提供了处理和操作文件路径的功能&#xff0c;其中 path.join 和 path.resolve 是两个常用的方法。它们在处理路径时有不同的行为和用途: 功能概述 path.join()&#xff1a; 该方法主要用于将多个路径片段拼接成一个完整的路径字符串。它会正…

将Scrapy项目容器化:Docker镜像构建的工程实践

引言&#xff1a;爬虫容器化的战略意义在云原生与微服务架构主导的时代&#xff0c;​​容器化技术​​已成为爬虫项目交付的黄金标准。据2023年分布式系统调查报告显示&#xff1a;92%的生产爬虫系统采用容器化部署容器化使爬虫环境配置时间​​减少87%​​Docker化爬虫的故障…

Unity × RTMP × 头显设备:打造沉浸式工业远控视频系统的完整方案

结合工业现场需求&#xff0c;探索如何通过大牛直播SDK打造可在 Pico、Quest 等头显设备中运行的 RTMP 低延迟播放器&#xff0c;助力构建沉浸式远程操控系统。 一、背景&#xff1a;沉浸式远程操控的新趋势 随着工业自动化、5G 专网、XR 技术的发展&#xff0c;远程操控正在从…

HTTPS如何保障安全?详解证书体系与加密通信流程

HTTP协议本身是明文传输的&#xff0c;安全性较低&#xff0c;因此现代互联网普遍采用 HTTPS&#xff08;HTTP over TLS/SSL&#xff09; 来实现加密通信。HTTPS的核心是 TLS/SSL证书体系 和 加密通信流程。一、HTTPS 证书体系HTTPS依赖 公钥基础设施&#xff08;PKI, Public K…

数据的评估与清洗篇---清洗数据

处理前的准备 检查索引与列名 在处理内容之前,需要先看看索引或列名是否有意义,若索引和列名都是乱七八糟的,应该对他们进行重命名或者重新排序,以便我们理解数据。 清洗数据 清洗数据原则 针对数据内容,一般先解决结构性问题,再处理内容性问题。整洁数据的特点是: …