C++之list类(超详细)
在上一节中我们学习了STL中的vector这个容器,这节我们来学习一下另外一个常用的容器——list。
文章目录
前言
一、list的介绍
二、list的使用及相关接口
1.list的使用
2.list的迭代器使用
3.list的相关接口
3.1 list capacity
3.2 list element access
3.3 list modifiers
三、list与vector的对比
四、list的模拟实现
前言
我们在vector中就已经介绍了C++中引入了STL的方便,这里我们就不多赘述了。我们上节所学习的vector,这个容器使用于那个存储空间连续的结构,因为它的底层设计是数组实现的。对于那些存储空间不连续的结构,如我们在数据结构中所学习的链表,它是有一个一个的指针连接起来的,这里我们就来学习一下这种容器——list。
一、list的介绍

上面这个介绍是引入C++文档指导的,我们从上面的文档中,我们可以知道list就是一个允许在任意位置进行插入/删除的序列容器,它同样可以使用迭代器,因此我们可以通过迭代器轻松遍历list中的内容。

如上图所示,我们在C++中所学习的list就是我们在数据结构中学习的带头双向循环链表,因此我们可以往前遍历,也可以往后遍历。因此它的头结点是一个关键的结点,我们要注意,后面我们在list的模拟实现中会着重介绍这个的。
二、list的使用及相关接口
1.list的使用
我们现在如果想要使用C++中的STL都是需要提前写出它们的头文件,并把它们包含起来,那么在主函数中,我们就可以直接使用它们了。list的头文件包含就是#include<list>,我们包含这个头文件之后,我们不仅可以使用list的迭代器还可以使用它的各种接口。
对于list对象的创建和之前的vector是一样的,我们在实例化对象时,我们写类类型的时候,我们要将类模板与数据类型一起写上去,这样才能够算是一个数据类型。
template<class T> //我们定义类模板中的元素类型
list<T> //这就是一个list对象的数据类型

如上图所示,我们可以构造一个空的list,也可以构造一个包含n个值为val的元素的list,我们还可以使用迭代器区间中的元素来构造list,除了上面哪几种构造函数,我们其实还可以使用初始化列表来初始化list中的内容,初始化列表就是使用一对{ },里面放置的是我们想要进行初始化的内容。(这种构造方法我觉得挺香的,可以自己来初始化想要的内容,不然我们还得创建一个空的list,然后再逐个插入元素,才能得到我们想要的list)

2.list的迭代器使用
这里我们可以将迭代器认为是一个指针,但是它实际上比不是指针,只是功能与指针很相似,这点我们需要注意,我们学习就将它们进行一下类比学习。list中的迭代器与我们vector迭代器不一样,由于vector中的存储空间是连续的,因此它的迭代器就相当于原生指针,使用起来十分方便,但是list的存储空间不连续,那么它的迭代器就不能用原生指针来代替。这里我们只是来初步介绍一下如何使用list的迭代器,至于list中的迭代器是如何实现的,我们在后面的模拟实现中会着重讲解的。
对于容器的使用方式都是一个模板,我们首先先使用我们想要的容器类型来定义一个迭代器变量并给它初始化,我们一般把第一个迭代器作为初始值。后面我们根据自己的需求来使用迭代器。对于list的迭代器,我们就将容器类型换成list<int>即可。如下代码是我们使用list迭代器进行遍历访问list中的元素。

对于迭代器的函数基本都是一样的,如下图中的那几个函数就是迭代器的接口函数,我们根据不同的场合进行使用。

注意:
1.begin,end是正向迭代器函数,我们实行++操作的时候,迭代器向后移动;
2.rbegin,rend是反向迭代器函数,我们实行++操作的时候,迭代器向前移动。
3.list的相关接口
3.1 list capacity

对于list容器,它没有像vector,string那样的capacity函数,因为list是一个存储空间不连续的结构,因此它的容量大小,我们无法通过函数接口来获取,我们只能得到它当前的元素个数size,以及list是否为空。这两个接口使用起来也是十分简单的,和之前几种容器的使用方法一样,我们直接调用函数即可。

3.2 list element access

我们在介绍list的时候就已经说了list是一个带头双向循环链表。因此对于它有两个元素我们是可以直接获取的:表头元素,表尾元素。我们可以分别使用front和back这两个函数来获取。

3.3 list modifiers

list中那些增删函数与vector中的函数接口基本差不多,两者上面主要的区别就是list可以在表头,表尾进行插入/删除,而vector只能够进行尾删/尾插操作。它们两个都没有了find函数了(string类中有find函数),它们如果想要使用这个函数,可以直接使用库中的find函数,对于库中的find函数,原型如下所示:

输入参数:我们要输入一个迭代器区间,还有一个我们想要查找的值。我们一般使用这个函数使用下面这个模板(以list举例):
#include<algorithm>
#include<iostream>
#include<list>
using namespace std;
int main()
{int x;cin >> x;auto it = find(lt.begin(), lt.end(),x); //auto 推导出来的是迭代器类型return 0;
}
如下代码,是插入/删除等函数的代码演示:

对于insert/erase这两个函数,我要好好来介绍一下。这里list中的insert/erase函数,它们的返回值类型都是迭代器类型(这里vector中的也是一样的)。而且我们传递的参数也是迭代器类型的参数或者迭代器区间。在vector中对于insert/erase中都会出现迭代器失效的情况,但是在list中insert并不会出现迭代器失效的情况,只有erase才会使得迭代器失效。因为我们在前面已经说了list是一个带头双向循环链表,这里迭代器失效即迭代器所指的结点无效,即该结点被删除了。因此插入结点时迭代器是不会失效的(我们每次插入的结点都是一个新的结点,都是有效的迭代器),而且erase导致迭代器失效的也只是指向被删除的那个结点的迭代器失效了,其他的结点并没有被影响。

这是文档中list::insert的介绍,我们传递的参数是迭代器位置,我们插入的位置是我们传递的迭代器位置的前一个迭代器位置,这一点我们要记住。至于它的返回值就是我们插入的位置的迭代器。

这是文档中list::erase的介绍,我们传递的参数迭代器就是我们想要删除位置的迭代器,但是它的返回值我们要注意了,它的返回值是我们被删除位置的下一个位置的迭代器。我们在vector中就说过了如何解决迭代器失效的方法:更新迭代器的值。因此,我们为了保证迭代器不会失效,我们可以将erase的函数返回值赋值给迭代器,那样迭代器进行了更新,就不会出现迭代器失效的情况了。
void TestListIterator1()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };list<int> l(array, array+sizeof(array)/sizeof(array[0]));auto it = l.begin();while (it != l.end()){// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给
其赋值l.erase(it); ++it;}
}
// 改正
void TestListIterator()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };list<int> l(array, array+sizeof(array)/sizeof(array[0]));auto it = l.begin();while (it != l.end()){l.erase(it++); // it = l.erase(it);}
}
三、list与vector的对比
vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及 应用场景不同,其主要不同如下:

四、list的模拟实现
#pragma once#include<assert.h>namespace hjc
{// 惯例// 全部都是公有,一般用structtemplate<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& x = T()):_data(x), _next(nullptr), _prev(nullptr){}};// typedef list_iterator<T, T&, T*> iterator;// typedef list_iterator<T, const T&, const T*> const_iterator;template<class T, class Ref, class Ptr> //我们定义模板的时候,我们定义多个参数//我们除了定义元素类型的参数,还定义一个引用类型的参数和一个指针类型的参数//这个对于我们后面实例化出普通迭代器与const迭代器有着奇效struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self; //重命名的时候,我们要将上面模板中定义的参数都带上Node* _node;list_iterator(Node* node):_node(node){}//下面重载*和—>运算符的使用,我们要使用上面定义的参数作为返回值//因为,我们普通迭代器与const迭代器的区别就是在这解引用,我们const修饰的是迭代器所指向的内容即顶层constRef operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}//其余地方不变Self& operator++(){_node = _node->_next;return *this;}Self& operator--(){_node = _node->_prev;return *this;}Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}};template<class T>class list{typedef list_node<T> Node;public:/*typedef list_iterator<T> iterator;typedef list_const_iterator<T> const_iterator;*///本质上还是实现了两个迭代器的类,这里不过是我们传递给模板参数,然后编译器帮我们实例化出来两种迭代器typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}//begin,end函数需要我们自己来重新定义,上面模板生成的迭代器,只是对那些运算符进行了重载const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}void empty_init(){_head = new Node();_head->_next = _head;_head->_prev = _head;_size = 0; //初始化时,size=0}list(){empty_init();}// lt2(lt1) 拷贝构造,我们传递的参数是类类型的引用类型//我们先对其初始化,然后我们将值逐个插入到我们要拷贝的链表中list(const list<T>& lt){empty_init();for (auto& e : lt){push_back(e);}}// lt2 = lt3//list& operator=(list lt)//对于赋值运算符的重载,我们可以直接将两个链表中的内容交换(交换一下头指针即可)list<T>& operator=(list<T> lt){swap(lt); //使用我们自己实现的swap函数 这里实际是 this.swap(lt)return *this; //这是赋值后的链表内容,原来的链表内容交换给了lt//由于lt是一个局部变量,在这个函数结束的时候,然后它就自动销毁了}//~list(){clear();delete _head;_head = nullptr;}//链表中的交换函数(直接交换两个头指针即可)std中的swap函数则需要重新创建两个空间,然后分别将值拷贝过去void swap(list<T>& tmp){std::swap(_head, tmp._head);std::swap(_size, tmp._size); //两个链表中的有效元素个数也要进行交换}//使用迭代器+erase直接将链表中的元素都删除掉,但是这里可能会导致迭代器失效的情况,因此我们需要对迭代器进行更新//erase函数的返回值类型是迭代器类型,返回的迭代器是被删除的紧接着的下一个迭代器位置void clear(){auto it = begin();while (it != end()){it = erase(it); //因此我们直接将erase的返回值赋给it,这样就相当于对迭代器更新+向后移动一位了}}//构造函数(构造n个相同值)list(size_t n, const T& val = T()){empty_init();//初始化 for (size_t i = 0; i < n; i++) //由于这里已经明确给了多少个元素了,于是我们就使用普通的for循环来插入值{push_back(val);}}//尾插 我们首先要定义一个要插入的新结点,然后找出插入结点与头结点等结点的关系//使用使用指针连续关系即可void push_back(const T& x){//定义两个新的结点/*Node* new_node = new Node(x);Node* tail = _head->_prev;//首先处理中间的tail->_next = new_node;new_node->_prev = tail;//然后处理后面的new_node->_next = _head;_head->_prev = new_node;*/insert(end(), x); //我们可以直接使用insert来进行复用//end()即_head位置,这里是头结点之前的位置,即链表尾的位置}void push_front(const T& x){insert(begin(), x); //begin()=>_head->_next,头结点的下一结点位置}void pop_front(){erase(begin());}void pop_back(){erase(--end()); //--end()指向的是头结点的上一个元素,即链表的最后一个元素}//插入位置是我们指定位置(pos)之前一个位置插入元素iterator insert(iterator pos, const T& val) {Node* cur = pos._node;Node* newnode = new Node(val);Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);}//返回的位置为被删除位置的下一个迭代器位置iterator erase(iterator pos) //参数即为要删除位置{assert(pos != end()); //我们不可以删除头结点位置的值(头结点指向的内容不是有效内容Node* del = pos._node;Node* prev = del->_prev;Node* next = del->_next;prev->_next = next;next->_prev = prev;delete del;--_size;return iterator(next);}private://这里,我们定义了两个成员变量,一个头指针,一个是元素的有效个数//size我们直接定义为成员变量,在上面的函数中如果有修改的地方我们就直接进行修改Node* _head;size_t _size;};//函数模板,这是std库中的//这个函数模板需要重新创建空间,然后拷贝内容过去template <class T>void swap(T& a, T& b){T c(a); a = b; b = c;}//这是list库中的swap函数模板template <class T>void swap(list<T>& a, list<T>& b) //传递两个链表类型的引用作为参数{a.swap(b); //实际是对我们上面那个swap的函数的一个封装,写这个函数是想让编译器优先使用这个函数,而不用上面那个函数模板}
}
对于上面的list的模拟实现,我主要讲解一下,迭代器的模拟实现。由于list是个链表由一个个的节点所组成的,因此我们并不能够像之前的vector那样,直接使用原生指针typedef即可。我们需要使用类来进行封装。我们要将迭代器的那些操作(运算符的重载)都封装到一个类中,但是迭代器又有还几种迭代器(这里我们就实现比较简单的迭代器:普通迭代器和const迭代器,至于方向迭代器我们这里就暂时不模拟实现)我们知道两种迭代器的实现功能都是不一样的但是又很相似,如果我们为了const迭代器再重写一个类,这样就显得有点冗余(其中许多操作都是相似的,只是几处有所差别)于是,我们就在模板上做点手脚:我们在模板参数中多设定几个参数:一般我们写模板参数时只写一个参数:类中对象的数据类型,这里我们加了两个参数:一个引用类型的参数Ref,一个指针类型的参数Ptr。对于const迭代器,const修饰的是迭代器所指向的内容,我们并不能简单的使用const来修饰我们最初模拟实现的普通迭代器然后typedef就行了。我们需要修改它们解引用操作的那几个运算符,因此我们直接在模板中给定参数,在我们实例化时,我们给定我们想要的参数就行了。其实,我们在模板中多设定几个参数,然后再传递参数来实例化就是让编译器帮我们写两个迭代器的类,这样的粗活就不用我们自己亲自来做了。
相关文章:
C++之list类(超详细)
在上一节中我们学习了STL中的vector这个容器,这节我们来学习一下另外一个常用的容器——list。 文章目录 前言 一、list的介绍 二、list的使用及相关接口 1.list的使用 2.list的迭代器使用 3.list的相关接口 3.1 list capacity 3.2 list element access 3.3…...
强化学习的一些概念
目录 强化学习 打个比方 核心要素 State Action Reward 几个代码demo 学习目标 强化学习 强化学习(Reinforcement Learning, RL)是机器学习的一个分支,旨在让智能体(Agent)通过与环境的交互学习最优策略,以…...
MambaTab:表格数据处理的新利器
——基于结构化状态空间模型的特征增量学习框架 摘要 本文提出MambaTab,一种基于结构化状态空间模型(SSM)的表格数据处理框架。通过创新的嵌入稳定化设计与轻量化SSM架构,MambaTab在普通监督学习和特征增量学习场景中均表现优异&…...
Kafka的流量控制机制
Kafka的流量控制机制 Kafka 作为一款高吞吐量的消息队列系统,能够在海量数据场景下提供稳定的消息生产和消费能力,其背后的流量控制机制功不可没。我们需要认识到,Kafka 的流量控制并非仅仅是为了防止系统过载或崩溃,它的目标是实…...
RabbitMQ支持的复杂的消息交换模式
RabbitMQ支持多种复杂的消息交换模式,这些模式通过不同的交换机类型和队列特性实现,能够满足多样化的业务需求。以下是RabbitMQ支持的主要复杂消息交换模式: 1. Direct Exchange(直连交换机) 直连交换机根据消息的路由…...
CSSHTML新特性
HTML5 新特性探秘 在 Web 开发的不断演进中,HTML5 带来了一系列令人振奋的新特性,极大地提升了网页的功能和用户体验。今天,我们就来深入探究一下这些新特性。 语义化标签:让网页结构更清晰 语义化标签是 HTML5 的一大亮点。在…...
51单片机的工作方式
目录 一、51 单片机的时钟电路及时钟信号 (一)时钟电路 (二)时钟信号 二、51 单片机的CPU 时序 (一)时钟周期 (二)机器周期 (三)指令周期 三、…...
Java算法OJ(12)
目录 1.前言 2.正文 2.1Fib数列 2.2单词搜索 2.3杨辉三角 3.小结 1.前言 哈喽大家好吖,今天来分享几道的练习题,欢迎大家在评论区多多交流,废话不多说让我们直接开始吧。 2.正文 2.1Fib数列 题目:斐波那契数列_牛客题霸…...
MrRobot靶机详细解答
一、主机发现 arp-scan -l二、端口扫描、目录枚举、指纹识别 2.1端口扫描 nmap -p- 192.168.55.147发现22端口关闭,且无其它特殊端口,只能去网页中寻找信息 2.2目录枚举 dirb http://192.168.55.1472.3指纹识别 nmap 192.168.55.147 -sV -sC -O --…...
java线性表(单向链表)
对于链表我们有很多种,有带头和不带头,双向和单项,循环和不循环。 我们实现的单向链表是不带头单向不循环链表。 链表不比顺序表,它可以连续也可以不连续,是链子型的每条链子两边都有节点(除首尾)。 单向…...
QT:动态属性和对象树
动态对象 1.添加Q_PROPERTY对象 #ifndef MYPROPERTYCLASS_H #define MYPROPERTYCLASS_H#include <QObject>class MyPropertyClass : public QObject {Q_OBJECTQ_PROPERTY(QString mask READ mask WRITE setMask NOTIFY maskChanged) public:explicit MyPropertyClass(Q…...
编程语言的几种常见的分类方法
一、 按照编程范式分类 命令式编程语言 强调通过语句来改变程序状态,如 C、Pascal、Fortran 等。 面向对象编程语言 基于对象和类的概念,支持封装、继承和多态,如 Java、C、Python、Ruby 等。 函数式编程语言 注重不可变性和纯函数…...
算法——层序遍历和中序遍历构造二叉树
晴问 #include <iostream> #include <vector> #include <queue> #include <unordered_map>using namespace std;struct TreeNode {int data;TreeNode *left;TreeNode *right;TreeNode(int data) : data(data), left(nullptr), right(nullptr) {} };//…...
中考英语之06语态(动词语态-简单)
1 主动语态和被动语态 在初中英语中,语态是动词的一种形式,用于说明主语与谓语动词之间的关系,主要有主动语态和被动语态两种。 1.1 主动语态 定义:表示主语是动作的执行者。结构:主语 谓语(及物动词&a…...
linux常用基础命令_最新
常用命令 查看当前目录下个各个文件大小查看当前系统储存使用情况查看当前路径删除当前目录下所有包含".log"的文件linux开机启动jar更改自动配置文件后操作关闭自启动linux静默启动java服务查询端口被占用查看软件版本重启关机开机启动取别名清空当前行创建文件touc…...
PH热榜 | 2025-03-16
1. BrowserAgent 标语:基于浏览器的人工智能代理 - 无限使用,固定费用 介绍:在您的浏览器中直接创建和运行AI工作流程,无需支付API费用。我们的可视化编辑器不需要编写代码,同时我们的浏览器本地技术支持以固定价格进…...
《论语别裁》第01章 学而(27) 无所适从的礼俗
下面讲做学问的态度。 有子曰:礼之用,和为贵,先王之道,斯为美,小大由之;有所不行,知和而和,不以礼节之,亦不可行也。 为什么讲学问讲到礼?这个礼,…...
关于进程的实验(子进程和父进程相关的)
文章目录 1.第一个问题2.第二个问题3.第三个问题 1.第一个问题 编写一段程序,利用系统调用fork( )创建两个进程。当此程序运行时,在系统中有一个父进程和两个子进程活动。让每一个进程在屏幕上显示一个字符:父进程显示字符“a”;子进程分别显…...
《基于视频监控的智能跌倒检测系统》开题报告
一、本课题研究目标 本课题旨在研究并实现一个基于视频监控的智能跌倒检测系统,以有效识别老年人或需特殊照顾人群的跌倒事件,并实现自动报警功能。具体而言,该系统将通过机器学习和深度学习技术,对视频数据中的行为模式进行分析&…...
Flask中的装饰器
在 Flask 中,装饰器(Decorator)是一种 Python 语法特性,它允许你在不修改原始函数的情况下,扩展其功能。Flask 使用装饰器来定义路由、请求前后钩子、中间件等。 1. Flask 装饰器的基本概念 Python 的装饰器本质上是一…...
MySQL配置文件my.cnf详解
目前使用的服务器系统是CentOS8.5 ,针对MySql8.4的配置示例,自己根据实际情况修改。 安装MySql8.4时,MySql8.4没有默认的my.cnf,需要用户根据需要自行配置my.cnf文件,大概可看到下面这样的参数列表,可能不同版本的mysql参数多少会…...
SonarQube安装及结合IDEA使用详细教程(2025适配版)
一、环境验证与准备 JDK版本确认 PS C:\> java -version openjdk version "17.0.14" 2025-01-21 LTS OpenJDK Runtime Environment Zulu17.5615-CA (build 17.0.147-LTS) OpenJDK 64-Bit Server VM Zulu17.5615-CA (build 17.0.147-LTS)安装路径说明 主程序路径&a…...
2018年全国职业院校技能大赛高职组-计算机网络应用竞赛竞赛样题E卷
目录 总体规划 模块二:设备基础信息配置 模块三:网络搭建与网络冗余备份方案部署 模块四:移动互联网搭建与网优 模块五:出口安全防护与远程接入 总体规划 医院在进行网络部分信息化建设方案设计过程中,需要保证医院、血液中心通过社保网进行互连互通,同时满足献血中心与医…...
python列表基础知识
列表 创建列表 1.列表的定义:可变的,有序的数据结构,可以随时添加或者删除其中的元素 2.基本语法:字面量【元素1,元素2,元素3】使用[]创建列表 定义变量:变量名称【元素1,元素2&…...
vue echarts封装使用
echarts 尺寸自动调节 resize.js 柱状图 components/dashboard/lineChart.vue <template><div :class"className" :style"{height:height,width:width}" /> </template><script> import echarts from echarts require(echarts/…...
PTP协议赋能高精度时间同步网络
什么是PTP? PTP(精确时间协议,Precision Time Protocol) 是一种基于IEEE 1588标准的网络时间同步协议,旨在为分布式系统中的设备提供亚微秒级(甚至纳秒级)的高精度时钟同步。其核心目标是通过消…...
使用WireShark解密https流量
概述 https协议是在http协议的基础上,使用TLS协议对http数据进行了加密,使得网络通信更加安全。一般情况下,使用WireShark抓取的https流量,数据都是加密的,无法直接查看。但是可以通过以下两种方法,解密抓…...
MIDI,AI 3D场景生成技术
MIDI(Multi-Instance Diffusion for Single Image to 3D Scene Generation)是先进的3D场景生成技术,能在短时间内将单张图像转化为高保真度的3D场景。通过智能分割输入图像,识别出场景中的独立元素,再基于多实例扩散模…...
三分钟掌握视频剪辑 | 在 Rust 中优雅地集成 FFmpeg
前言 在当今的短视频时代,高效的视频剪辑已成为内容创作者和开发者的迫切需求。无论是裁剪视频开头结尾、提取高光时刻,还是制作 GIF、去除广告,剪辑都是必不可少的一环。 然而,批量处理大量视频并非易事,常见的挑战…...
Linux 快捷键 | 终端快捷键 / 键盘快捷键
注:本文为 “Linux 快捷键” 相关文章合辑。 英文引文,机翻未校。 未整理去重。 Linux 终端常用快捷键 组合键 ~~~~~~~ 功能描述Ctrl a光标移动到行首(Ahead of line),相当于通常的 Home 键Ctrl b光标往回 (Back…...
