【C++学习】list的使用及模拟实现

news2024/11/14 21:09:36

🐱作者:一只大喵咪1201
🐱专栏:《C++学习》
🔥格言:你只管努力,剩下的交给时间!
tu

list的使用及模拟实现

  • 😼构造函数
    • 🐵模拟实现
  • 😼迭代器
    • 🐵普通迭代器(iterator)模拟实现
    • 🐵const迭代器(const_iterator)模拟实现
    • 🐵箭头(->)运算符重载
  • 😼赋值运算符重载
    • 🐵模拟实现
  • 😼析构函数
    • 🐵析构函数存在的必要性
  • 😼容量和访问操作
    • 🐵容量操作
    • 🐵访问操作
  • 😼增删查改操作
  • 😼其他几个常用接口
  • 😼list和vector优劣比较
  • 😼总结

list和vector一样,也是一个非常常用的数据结构,它和vector就像左手和右手的关系,优略势互补,它的本质是一个带头双向循环列表,下面本喵来给大家详细介绍一下list。

😼构造函数

图
官方提供的构造函数有四个,其中包括拷贝构造函数,如上图所示。

void list_test1()
{
	list<int> lt1;
	list<int> lt2(5, 1);

	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	list<int> lt3(v1.begin() + 1, v1.end() - 1);
	list<int> lt4(lt2);
}

图
通过调试的监视窗口可以看到,使用四种方法都能成功创建list对象。

为了更好的理解list,下面来模拟实现一下它。

🐵模拟实现

图
如上图,list是又一个又一个的节点构成,每个节点中,不仅存放数据,而且存放着前一个结点和后一个节点的指针。

所以我们需要创建一个类,这个类就是用来描述节点的。

namespace wxf
{
	template <class T>
	struct __list_node
	{
		__list_node* prev;//指向前一个节点的指针
		__list_node* next;//执行下一个节点的指针
		T data;//根据模板参数实例化,存放数据
		__list_node(const T& x)
			:prev(nullptr)
			,next(nullptr)
			,data(x)
		{}
	};
}

同样将我们自己模拟实现的部分放在命名空间wxf中。

  • 这里节点类创建时,使用的关键字是struct,而不是class。
  • 使用struct创建的类,默认所有成员都是公有的,如果使用class,默认所有成员是私有的,需要在所有成员变量之前加public。

每一个节点在创建时,需要进行初始化,所以在节点类中需要有一个构造函数,来初始化节点。

构造函数:

图
为了使用方便,将前面的节点类使用typedef重命名为node,在list类中,成员变量只有一个,就是节点的指针变量_head,也就是头节点的地址。

  • 类名<模板参数>等价于类型,所以在typedef的时候,只写类名__list_node是不行的。

每创建一个list对象的时候,首先要做的事情就是new一个头节点,由于此时list是空的,所以头节点的next和prev都是指向自己的,如上图绿色框中的代码所示。

push_back():

需要一个接口来向list中插入数据,这里先实现最常用的push_back。

图
每次尾插一个数据的时候,就需要动态开辟一个节点对象,然后再将_head,tail,newnode三个节点按照带头双向循环列表的规则进行链接。

图
如上图,在尾插一个新节点的时候,由原本绿色的链接关系变成了蓝色的链接关系。

图

成功的使用我们自己实现的构造函数创建了一个list对象,并且尾插了五个数字,如上图所示。

继续实现构造函数:

图
使用多个相同的数据来构造list对象的的构造函数,本质就是进行多次尾插,如上图中的红色框。

图
通过监视窗口看到,符合我们对预期。

每次构造一个对象的时候,必须先初始化头节点,为了简化代码,将头节点初始化的代码放在一个函数中。

图
在每个构造函数中调用这个初始化函数就可以进行头节点的初始化。

😼迭代器

先看它的使用:

void list_test2()
{
	list<int> lt1;
	lt1.push_back(1);
	lt1.push_back(2);
	lt1.push_back(3);
	lt1.push_back(4);
	lt1.push_back(5);
	
	list<int>::iterator it1 = lt1.begin();
	while (it1 != lt1.end())
	{
		cout << *it1 << " ";
		++it1;
	}
	cout << endl;
}

tu
使用迭代器成功打印出了list中的每一个数据。

无论是哪种数据结构,都有迭代器,在前面我们学习vector和string的时候就学习到了迭代器,我们知道它是一个行为上像指针一样的东西,由于vector本身的物理结构是连续的,所以它的迭代器本质上就是一个指针。

但是list的物理结构是不连续的,它的本质注定不能是一个指针,下面本喵来详细介绍。

🐵普通迭代器(iterator)模拟实现

  • 迭代器有++或者–等行为,由于list的物理空间不连续,所以此时的++和–需要重新定义,也就是需要进行运算符重载。

为了让它行为上像指针一样,只能通过一个类来描述它,所以说,list的迭代器是一个类对象。

	template <class T>
	struct __list_iterator
	{
		typedef __list_node<T> node;
		node* _pnode;

		__list_iterator(node* p)
			:_pnode(p)
		{}
	};

创建一个迭代器类,如上面的代码,类中只有一个成员变量,用了存放节点的地址。同样为了能够直接访问,使用struct创建类。

同样,必须有构造函数,编译器自动生成的默认构造函数不能将pnode初始化为我们想要的,所以需要自己实现一个。

要想让list的迭代器也能像指针一样进行++或者–,需要对运算符进行重载。
图
如上图中,分别是前置的++和–以及厚置++和–的运算符重载。

  • 前置++返回的是++后的结果,所以返回this指针的内容,并且使用的是引用返回,因为这里的迭代器对象不会销毁。
  • 后缀++返回的是++前的结果,所以需要一个临时对象,来存放++之前的结果,然后再进行++,但是,这里不能用引用返回,临时对象出了作用域以后就会销毁。

上面四个运算符重载函数中,无论是返回类型,还行创建的临时变量,都需要写很长一个串类型名(__list_iterator),为了简化,我们这里将其重命名成一个简单的类型名。(这一步操作后面有大用处)

typedef __list_iterator<T> Self;

并且将前面代码中的__list_iterator替换为Self。

既然像指针一样,那迭代器就必须能解引用,因为list的迭代器是一个类对象,所以解引用同样需要进行运算符重载。

图
由于解引用以后会对list中的数据进行访问,所以使用引用返回。

图

上图中是对==和!=进行的运算符重载,这俩个符合也是非常常用的。

图
将迭代器本身处理好以后,还需要让它在list类域中能够找到,如上图红色框中代码,在list类中,将迭代器重命名为我们熟悉的iterator。

  • 这里的重命名必须设置为公有,否则我们无法创建itertator迭代器,因为私有在类外是无法访问的。

图
在list类中还需要提供begin和end接口,如上图所示,这是在list类中实现的,而不是在__list_iterator中实现的。

图
此时,我们模拟实现的迭代器同样可以正常使用了。

🐵const迭代器(const_iterator)模拟实现

当一个对象是被const修饰的时候,普通迭代器就不再适用了,需要用到const迭代器。

图
此时我们创建的是一个const对象,但是发现通过它的迭代器,该对象不仅可以访问,而且还可以修改,这和const的性质相悖,const修饰的对象也失去了它应有的意义。

我们首先要做的事情就是不能让它修改。此时就有一种改法:

const list<int>::iterator it = lt1.begin();

用const修饰it,此时不就行了吗?

答案是不行的。此时运行会报错,大家可以自己尝试一下。

  • 此时const修饰的是it本身,会导致it不能被修改,也就是it不能进行++和减减等操作。
  • 并且此时*it还行可以进行++。

这和我们使用const的初衷不符合,我们是为了防止修改it指向的对象,而不是it本身。

既然这样,我们让*it返回的数据不能被修改。

图
如上图,在迭代器类中,对*的运算符重载,返回的数据类型是const T&,此时就无法修改了。

道理是这么个道理,但是此时就存在了两个operator*运算符重载函数:

图
在运行的时候,会报错。

  • 返回类型不同不能构成远算符重载。

为了解决这个问题,需要再创建一个类,创建一个const_iterator的专属类。

图

此时普通迭代器类和const迭代器两个类,除了类名不一样以外,只有解引用运算符重载(operator*())不一样,其他都一样。

图

并且在list类中,也将两种迭代器typedef为我们熟悉的样子。

图
此时it就不能被修改了,符合了const的性质。

虽然实现了,但是总感觉有不舒服,因为太冗余了,就是因为一个函数的返回类型而重新写了一个类,可以采用下面的办法来解决。

图
迭代器的类模板,使用两个模板参数,解引用运算符重载的返回类型使用第二个模板参数Ref,如上图红色框所示。

图
在list类中,给迭代器类不同类型的模板参数,并且使用typedef进行重命名,如上图红色框中所示。

  • 此时,使用哪种迭代器,就会使用哪种模板参数进行实例化,相当于将前面我们写的俩个迭代器类的工作交给了编译器来完成。

图

同样还需要写上图中的两个成员函数在list类中,此时const迭代器才能正常的使用begin和end。

🐵箭头(->)运算符重载

图
当放入list的数据时一个结构体类型的时候,如上图红色框所示,此时想要打印出具体数据是做不到的,因为流插入运算符(<<)不支持打印结构体类型的数据,如上图中绿色框中所示。

要想打印,此时该怎么办呢?

图
可以像上图中那样,先将it解引用拿到结构体数据,然后使用点操作符打印出行和列。

既然迭代器it的行为像指针,理论上我们也可以通过箭头(->)操作符来访问结构体类型数据。

图
将像这样,但是此时很显然,是无法实现的,需要我们进行箭头运算符的重载。

图
在迭代器类中进行运算符重载,箭头运算符重载返回的是数据的地址,如上图所示。

图
此时成功打印出了一组坐标。而且使用的是箭头操作符,行为完全符合指针。

但是此时又有一个疑问:

图
将该运算符重载函数现实调用以后,如上图中的红色框,得到的是数据所在的地址,再加一个箭头,如上图中绿色框,才能访问到结构体中具体成员。这样来看,一共加起来两个箭头,但是直接使用重载后的运算符,只需要it->一个箭头就可以。

  • 为了提高可读性,编译器进行了简化,将原本的两个箭头简化成了一个箭头。

在箭头运算符重载函数中,返回类型是T*,同样的,如果是一个const对象呢?继续使用T又不符合const的性质,const T 返回类型又构不成重载,为了解决这个问题,可以参考两种迭代器的解决办法。

将迭代器类模板的模板参数再增加一个:

图

将模板参数的第三个参数作为operator->()运算符重载函数的返回类型。

图
在list类中,普通迭代器第三个模板参数就传T*,const迭代器,第三个模板参数就传const T*,让编译器根据具体情况去实例化。

图
前面本喵就说过,这个地方会有大用,当模板参数有了变化以后,只需要在红色框中改变就行,在后面用到类型的时候,由于typedef的存在,直接使用Self即可。

到这里,我们就将迭代器部分模拟完了,反向迭代器本喵就不再模拟了,我们仅是为了更好的学习迭代器,并不是为了造一个更好的轮子。

使用迭代器区间的构造函数:

😼赋值运算符重载

图

赋值运算符重载函数只有这一个,没有重载。

图
上图中,创建了lt1和lt2两个list对象,其中lt2是空的,然后将lt1的值全部赋值给lt2,从运行结果中可以看到,此时两个对象的值是一样的。

🐵模拟实现

在模拟实现赋值运算符重载之前,需要做一些准备工作,同样这些接口也是我们常用的,在这里就一并模拟实现了。

erase():

图
eraes的本质就是将prev,next直接链接上,从而跳过pos,并且最后释放pos所在的节点。为了防止出现迭代器失效,这里返回pos的下一个位置,也就是next节点。

clear():

图
clear的作用就是只保留list的头结点,将其余节点全部释放掉。

准备工作做好以后就可以模拟实现了:

传统写法:

图

  • 如果是自己给自己赋值的话,直接返回自己本身就可以。
  • 它的本质就是被赋值一方只保留头结点,其他节点全部删除,然后再逐个尾插。
  • 使用范围for的时候,为了提高效率,使用的是auto&。

现代写法:

同样需要做一个准备工作,需要写一个拷贝构造函数,也正回补上了前面的遗漏。

拷贝构造函数:

图
使用范围for进行尾插。

图
还需要我们自己实现一个交换函数,只需要交换两个list的头结点就可以。

  • 算法库中的swap函数直接使用的话,会导致效率低下,因为算法库中是交换所有的内容。

图

  • 形参不能使用list,因为不能将this的_head换给原本提供数据的list对象。
  • 此时的val是一个临时变量,this中的_head和它交换以后,val会被释放。

😼析构函数

先来看析构函数的实现:

图

析构函数首先要做的是除了头结点以外的其他节点释放,最后再释放头结点,否则就会找不到其他节点。

🐵析构函数存在的必要性

图

节点类和迭代器类,就没有析构函数,这是为什么?他们不需要释放吗?

  • 对于迭代器,它的成员只有一个指针,所以就存放在栈区就可以,也不会占用太多的内存,不用在堆区开辟动态空间,所以不需要析构函数。
  • 对于节点类,它本身并不会创建类对象,而是list类会按照它的蓝图在堆区创建一个节点对象。所以说,它是由list类对象控制的,包括创建和释放,所以节点类本身不需要析构函数。

甚至他们仅有一个构造函数,都不用考虑深层次拷贝的问题,这是为什么?

  • 对于迭代器,例如语句iterator it = lt.begin(),它本质上使用的是一个拷贝构造函数,但是我们并没有写,这是因为编译器自动生成的默认拷贝构造函数就能满足要求,此时迭代器需要的就是完全复制lt.begin()的地址。
  • 对于节点类,对具体的节点操作,只有创建和销毁,还有访问,至于赋值什么的主要操作的是头结点,所以它没有必要进行深层次拷贝。

😼容量和访问操作

🐵容量操作

图
需要增加一个成员变量_size,用来记录节点个数。同样的,在一些增加和减少成员函数中,也要改变_size的值。

🐵访问操作

链表并不支持随机访问,只有访问头和尾的成员函数。

图
如上图所示,是访问头和尾的普通对象和const对象调用的成员函数。

图
上图是对应的运行结果。

😼增删查改操作

insert():

图
插入一个新的节点,本质上就是在修改新节点以及该位置前后俩个节点共三个节点的链接关系。每插入一个节点,数据个数要加1。为了防止迭代器实失效,最后返回的是新插入节点的位置。

图
此时push_back也可以进行复用,使用insert在末尾插入。

push_front():

tu
同样在复用insert。

图
成功头插了五个数字。

pop_back()和pop_front():

tu
这俩个函数只需要复用erase即可。

图

😼其他几个常用接口

sort():

list还提供了一个排序接口。

图
可以看到,将一组乱序的数字排成了有序。

unique():

tu
该函数的作用就是去重,将重复的数据去掉。

  • 去重之前必须先让它有序,所以要在unique之前使用sort来排序。

remove():

图
该函数的作用就是去除掉链表中指定的数据。

  • 指定数据如果有多个,就会有多少去除掉多少。
  • 指定数据如果没有,那么它什么也不会干,也不会报错。

merge():
图
将两个链表有序合并。

😼list和vector优劣比较

vector

优点:

  1. 支持下标随机访问。
  2. 尾插尾删效率高(偶尔扩容除外)。
  3. cpu高速缓存命中率高。

缺点:

  1. 前面部分插入删除效率低。
  2. 扩容有消耗,而且存在一定的空间浪费。

list

优点:

  1. 按需申请释放,不需要扩容。
  2. 支持任意位置插入删除,时间复杂度是O(1)。

缺点:

  1. 不支持下标随机访问。
  2. cpu高速缓存命中率低。

list和vector就像左右手的关系,它们的优劣势互补。一般vector满足的场景下优先使用vector。

😼总结

list中,需要我们重点掌握的就是它的迭代器,因为这里的迭代器和之前vector已经string的不同,它是封装好的一个类。进行list的模拟实现,主要目的是为了更好的了解和使用list,并不是为了造一个更好的轮子,官方提供的STL模板已经非常完美了。

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

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

相关文章

[软件工程导论(第六版)]第6章 详细设计(复习笔记)

文章目录6.1 结构程序设计6.2 人机界面设计6.3 过程设计的工具6.3.1 程序流程图&#xff08;程序框图&#xff09;6.3.2 盒图&#xff08;N-S图&#xff09;6.3.3 PAD图&#xff08;问题分析图&#xff09;6.3.4 判定表6.3.5 判断树6.3.6 过程设计语言6.4 面向数据结构的设计方…

Spring Cloud Nacos实战(二)- 服务提供者注册

Nacos服务提供者注册 Nacos代替Eureka ​ Nacos可以直接提供注册中心&#xff08;Eureka&#xff09;配置中心&#xff08;Config&#xff09;&#xff0c;所以它的好处显而易见&#xff0c;我们在上节课成功安装和启动了Nacos以后就可以发现Nacos本身就是一个小平台&#xf…

解决IDEA报错:无效的目标发行版: 17

解决IDEA报错&#xff1a;无效的目标发行版: 17 目录解决IDEA报错&#xff1a;无效的目标发行版: 17报错由来解决报错【1】检查setting设置&#xff0c;查看编译器编译模块的编译版本是否是你需要的【2】尝试去修改当前项目的启动设置&#xff0c;设置JRE为你需要的版本。【3】…

odoo如何完成员工借款、报销、归还余款的会计操作?【业财一体】

ODOO中如何实现财务场景&#xff1a;一个员工从公司借款1000元&#xff0c;然后用这个借款&#xff0c;支付了电费500元&#xff0c;然后又归还剩余500元呢&#xff1f; 一、基本流程如下&#xff1a; 二、基础科目的设置 1.1 预付账款科目&#xff1a;设置在 产品【Employee…

DQL 多表查询

1、多表关系 一对多&#xff08;多对一&#xff09; 案例: 部门 与 员工的关系 关系: 一个部门对应多个员工&#xff0c;一个员工对应一个部门 实现: 在从表的一方建立外键&#xff0c;指向主表一方的主键 多对多 案例: 学生 与 课程的关系 关系: 一个学生可以选修多门课程&am…

Revisiting Distributed Synchronous SGD 带有Back-up机制的分布式同步SGD方法 论文精读

论文链接&#xff1a;Revisiting Distributed Synchronous SGD ABS 本文介绍了用于分布式机器学习的同步和异步SGDSGDSGD&#xff0c;同时指出各自的缺点&#xff1a;stragglersstragglersstragglers和stalenessstalenessstaleness。 同时为了解决同步SGDSGDSGD存在straggle…

【go语言grpc之client端源码分析二】

go语言grpc之server端源码分析二DialContextparseTargetAndFindResolvergetResolvernewCCResolverWrapperccResolverWrapper.UpdateStatecc.maybeApplyDefaultServiceConfigccBalancerWrapper.updateClientConnState上一篇文章分析了ClientConn的主要结构体成员&#xff0c;然后…

扫雷小游戏 - C语言

目录 一、扫雷游戏 1.游戏一共创建使用了三个文件 2.test.c 文件代码&#xff08;游戏逻辑&#xff09; 3.game.h - 必要的函数声明代码 4.game.c 游戏所有函数实现代码 一、扫雷游戏 1.游戏一共创建使用了三个文件 test.c - 测试游戏逻辑 game.h - 游戏代码包含的函数…

低频量化之指数 PE-PB-偏离 数据

目录历史文章股票明日涨停预测指数PEPB分位指数PE分位指数PB分位行业指数PEPB分位行业指数PE分位行业指数PB分位指数60日线偏离数据指数MA60偏离统计上证指数 MA60偏离度深证成指 MA60偏离度创业板指 MA60偏离度中小100 MA60偏离度上证50 MA60偏离度沪深300 MA60偏离度中证500 …

代码随想录算法训练营第45天动态规划 背包基础 1 2、 416. 分割等和子集

文章目录01背包基础 &#xff08;二维数组&#xff09;思路递推公式初始化遍历顺序一维dp数组&#xff08;滚动数组&#xff09;一维数组的递推公式遍历顺序LeetCode 416. 分割等和子集思路总结01背包基础 &#xff08;二维数组&#xff09; 思路 根据动态规划五部进行分析&a…

Vulnhub 渗透练习(八)—— THE ETHER: EVILSCIENCE

环境搭建 环境下载 靶机和攻击机网络适配都选 NAT 即可。 信息收集 主机扫描 两个端口&#xff0c;22 和 80&#xff0c;且 apache httpd 2.4.0~2.4.29 存在换行解析漏洞。 Apache HTTPD是一款HTTP服务器&#xff0c;它可以通过mod_php来运行PHP网页。其2.4.0~2.4.29版本中…

跨域问题的三种解决办法

我们平时对于前后端联调的项目&#xff0c;以下的错误是经常常见的&#xff0c;我们查看浏览器报错&#xff1a; Access to XMLHttpRequest at http://localhost:63110/system/dictionary/all fromorigin http://localhost:8601 has been blocked by CORS policy: No Access…

自动化测试5年经验,分享一些心得

自动化测试介绍 自动化测试(Automated Testing)&#xff0c;是指把以人为驱动的测试行为转化为机器执行的过程。实际上自动化测试往往通过一些测试工具或框架&#xff0c;编写自动化测试用例&#xff0c;来模拟手工测试过程。比如说&#xff0c;在项目迭代过程中&#xff0c;持…

【Flutter入门到进阶】Dart进阶篇---Dart多线程异步原理

1 Isolate 1.1 什么是Isolate 1.1.1 概念 线程&#xff1f;异步&#xff1f;隔离&#xff1f;到底什么意思&#xff1f; Isolate中文意思是隔离&#xff0c;从使用角度来说是Dart的线程&#xff0c;但是从本质虚拟机的实现角度来讲Isolate是一组封装。 isolate可以理解为dar…

群晖-第1章-IPV6的DDNS

群晖-第1章-IPV6的DDNS 方案&#xff1a;腾讯云群晖DS920 本文参考群晖ipv6 DDNS-go教程-牧野狂歌&#xff0c;感谢原作者的分享。 这篇文章只记录了我需要的部分&#xff0c;其他的可以查看原文&#xff0c;原文还记录了更多的内容&#xff0c;可能帮到你。 一、购买域名 …

【基于众包标注的语文教材句子难易度评估研究 论文精读】

基于众包标注的语文教材句子难易度评估研究 论文精读信息摘 要0 引言1 相关研究2 众包标注方法3 语料库构建3.1 数据收集3.1 基于五点量表的专家标注3.3 基于成对比较的众包标注4 特征及模型4.1 特征抽取4.2 模型与实验设计4.2.1 任务一:单句绝对难度评估4.2.2 任务二:句对相对…

《JavaScript百炼成仙》,简单但是挺有效的

编程之修&#xff0c;重在积累&#xff0c;而非资质。资质虽然重要&#xff0c;可是后天的努力更不可少。 《JavaScript百炼成仙》是一本以玄幻小说的形式&#xff0c;来讲述JavaScript的知识。 此篇仅仅是我快速阅读《JavaScript百炼成仙》这本书的笔记&#xff0c;流水账笔…

MySQL进阶篇之InnoDB存储引擎

06、InnoDB引擎 6.1、逻辑存储结构 表空间&#xff08;Tablespace&#xff09; 表空间在MySQL中最终会生成ibd文件&#xff0c;一个mysql实例可以对应多个表空间&#xff0c;用于存储记录、索引等数据。 段&#xff08;Segment&#xff09; 段&#xff0c;分为数据段&#x…

python基于vue微信小程序 房屋租赁出租系统

目录 1 绪论 1 1.1课题背景 1 1.2课题研究现状 1 1.3初步设计方法与实施方案 2 1.4本文研究内容 2 2 系统开发环境 4 2.1 2.2MyEclipse环境配置 4 2.3 B/S结构简介 4 2.4MySQL数据库 5 2. 3 系统分析 6 3.1系统可行性分析 6 3.1.1经济可行性 6 3.1.2技术可行性 6 3.1.3运行可行…

面试准备知识点与总结——(虚拟机篇)

目录JVM的内存结构JVM哪些部分会发生内存溢出方法区、永久代、元空间三者之间的关系JVM内存参数JVM垃圾回收算法1.标记清除法2.标记整理3.标记复制说说GC和分代回收算法三色标记与并发漏标的问题垃圾回收器项目中什么时候会内存溢出&#xff0c;怎么解决类加载过程三个阶段何为…