STL——list的介绍和模拟实现

news2025/3/4 16:38:29

前言

本篇博客我们将要开始介绍list这个容器,list是带头双向循环链表,STL标准模板库中实现了list这样方便我们去使用,那么本篇博客我们将脱下list的神秘外衣,介绍它的使用以及模拟实现。

list的介绍

list的底层是带头双向循环链表,可以在任意位置完成插入删除的功能,了解其迭代器我们将更加深入了解C++封装的特性。与vector相比,list的最大缺陷是不支持随机访问,但由于其底层封装了迭代器,因此list是可以通过迭代器访问的。同时,由于list的结构是链状的,所以它在一定程度上可以节省空间

下面我们来看一下list为我们提供的接口:

首先来看list的构造函数:

list与其他容器一样都提供了一个迭代器构造,initializer构造以及拷贝构造 

接下来是迭代器:

 容量操作:

修改操作:

 

list的模拟实现

这是这篇博客的重点,通过模拟实现list我们将更加深入了解list的底层结构,以及如何使用

节点:

首先我们提供一个节点的类,这个类里面包含了prev指针,next指针以及我们要存储的数据data,如下所示:

template<class T> 
struct ListNode
{
	ListNode<T>* _next;
	ListNode<T>* _prev;
	T _data;
	ListNode(const T& data = T()) :_next(nullptr), _prev(nullptr), _data(data);
	{}
};

分析上述代码,我们看到,我们将这个类声明为struct而不是class,这是为什么呢?因为在后续的操作过程中我们将频繁访问 节点当中的数据,如果声明成class,那么如果我们要访问内部的数据就要声明成友元或者内部类,这样会对阅读代码的人造成困扰,同时也不方便我们去操作,所以在实际操作过程中,如果看见了需要频繁操作的数据,那么我们尽可能的让它声明成struct

同时,我们在这个节点类中提供了一个构造函数,这样可以使我们的程序更加安全可靠。

list类

有了节点后我们将用这个节点去创建一个list,因此我们此时需要一个list类,里面封装我们要实现list的各种操作,其代码如下所示:

template<class T>
class list
{
	typedef ListNode<T> Node;
public:

private:
	Node* _head;
};

首先我们搭建出list的框架,里面封装了一个头节点,其后续要实现的各种功能我们将在public中实现。同时,我们将ListNode<T>重新命名为了Node,这样是为了可读性考虑

迭代器的实现

我们知道,迭代器为容器提供了一种统一的访问形式,从而屏蔽了底层细节,方便人们使用和学习。前面我们实现了vector,它的迭代器是一个指针,那么对于list而言我们能用指针实现吗?答案是否定的,因为它的结构不是连续的,所以我们不能这么做,那么为了保证我们有一种统一的访问方式我们必须提供一个类去封装iterator,这个类里面得重载前置++,前置--,后置++,后置--,迭代器,解引用等功能,其结构如下所示:

	template<class T>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T> Self;
		Node* _node;

		ListIterator(Node*node):_node(node)
		{}


	};

分析上述代码,我们可以看到,在这个迭代器类中我们保存了一个节点,以便访问节点中的数据,从而实现对节点的操作,由于后续要频繁访问数据,所以我们同样将ListIterator声明为公有。

前置++/--

Self& operator++()
{
	_node = _node->_next;
	return *this;
}
Self& operator--()
{
	_node = _node->_prev;
	return *this;
}

后置++/--

Self operator++(int)
{
	Self tmp(_node);
	_node = _node->_next;
	return tmp;
}
Self operator--(int)
{
	Self tmp(_node);
	_node = _node->_prev;
	return tmp;
}

重载*

T& operator*()
{
	return _node->_data;
}

重载->

T* operator->()
{
	return &_node->_data;
}

重载==/!=

bool operator==(const Self&it)
{
	return _node == it._node;
}
bool operator!=(const Self& it)
{
	return _node != it._node;
}

 由此,我们就完成了一个iterator,但是这样的代码显然移至性是很低的,为什么呢?因为在实际操作过程中,我们常常会遇见const修饰的对象,那么对于其而言我们应该使用const迭代器去完成,那这该怎么办呢?有两种解决方法,一种是重新实现一个const类型的迭代器;另一种通过调用不同的模板参数去完成,显然第一种方法虽然很简单,但是有很多重复的代码,所以我们在实际操作中更多的是用第二种解决方法。

仔细观察上述代码,我们发现只有函数可以用const修饰,一个是T&,一个是T*,那么我们可以再加两个模板参数,一个负责T&,一个负责T*,那么我们就得到了下面的代码:

template<class T,class Ref,class Ptr>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T,T&,T*> Self;
	Node* _node;

	ListIterator(Node*node):_node(node)
	{}

	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}
	Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	Self operator++(int)
	{
		Self tmp(_node);
		_node = _node->_next;
		return tmp;
	}
	Self operator--(int)
	{
		Self tmp(_node);
		_node = _node->_prev;
		return tmp;
	}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	bool operator==(const Self&it)
	{
		return _node == it._node;
	}
	bool operator!=(const Self& it)
	{
		return _node != it._node;
	}
};

begin()与end()

当我们将list的迭代器类实现出来后我们就可以用迭代器访问这个类,因此我们应该在list这个类中提供一个接口,这个接口就是begin与end,其代码如下所视:

typedef ListIterator<T, T&, T*> iterator;
iterator begin()
{
	return iterator(_head->_next);
}
iterator end()
{
	return iterator(_head);
}
typedef ListIterator<T, const T&,const T*> const_iterator;
const_iterator begin()const
{
	return const_iterator(_head->_next);
}
const_iterator end()const
{
	return const_iterator(_head);
}

list的插入/删除

list最常见的插入删除就是在头部和尾部的插入删除,其中插入的步骤是:

1.创建新节点

2.获取目标位置的节点及其前驱节点

3.让新节点的头指向目标位置节点的头节点,让新节点的尾节点指向目标位置节点

4.让前驱节点的尾指向新节点,目标位置节点的头指向新节点

值得注意的是:list的插入/删除伴随着迭代器失效的问题,即:插入新节点后无法访问插入位置的数据

那么解决方法是什么呢?很简单,就是返回插入位置的数据,因此list的插入和删除都要传迭代器接收目标位置的数据,代码如下:

iterator insert(iterator pos, const T& x)
{
	Node* newnode = new Node(x);
	Node* cur = pos._node;
	Node* prev = cur->_prev;

	newnode->_next = cur;
	newnode->_prev = prev;
	prev->_next = newnode;
	cur->_prev = newnode;
	return iterator(newnode);
}
iterator erase(iterator pos)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	prev->_next = next;
	next->_prev = prev;
	delete cur;
	return iterator(next);
}

有了插入删除的接口,那么我们就可以很轻松实现list的头部插入删除,尾部插入删除,其代码如下:

void pop_back()
{
	erase(--end());
}
void pop_front()
{
	erase(begin());
}

void push_back(const T& x)
{
	insert(begin(), x);
}
void push_front(const T& x)
{
	insert(end, x);
}

构造/析构函数

list的C++标准库中提供了很多的构造方式,我们在这里只着重介绍其中的无参构造,initializer_list构造以及拷贝构造,在实现构造函数之前,我们应该先写一个初始化函数,这个初始化函数是将头节点的next和prev都指向自己从而实现初始化,我们在写构造函数时,主要用这个初始化函数来构造,其代码如下:

void empty_init()
{
	_head = new Node();
	_head->_next = _head;
	_head->_prev = _head;
}
list()
{
	empty_init();
}
list(const list<T>& il)
{
	empty_init();
	for (const auto& e : il)
	{
		push_back(e);
	}
}
list(initializer_list<T> il)
{
	empty_init();
	for (const auto& e : il)
	{
		push_back(il);
	}
}

析构函数与构造函数类似,提供了一个clear接口,通过调用clear接口逐个删除list的节点,最后再将头节点释放,这样就实现了一个简单的析构函数,其代码如下所示:

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}
~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

重载=

list的重载=是通过传值传参然后调用swap来实现的,其代码如下所示:

list<T>& operator=(const list<T> lt)
{
	swap(_head, lt._head);
	return *this;
}

分析上述代码,我们可以看到,不同于传统的重载operator=我们这里采用了传值传参,然后交换头节点,这样做的好处显而易见,传值传参调用拷贝构造产生一个临时对象,临时对象具有常量属性因此要加const修饰,在出作用域之后就析构销毁。

list模拟实现代码如下

template<class T> 
struct ListNode
{
	ListNode<T>* _next;
	ListNode<T>* _prev;
	T _data;
	ListNode(const T& data = T()) :_next(nullptr), _prev(nullptr), _data(data)
	{}
};

template<class T,class Ref,class Ptr>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T,T&,T*> Self;
	Node* _node;

	ListIterator(Node*node):_node(node)
	{}

	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}
	Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	Self operator++(int)
	{
		Self tmp(_node);
		_node = _node->_next;
		return tmp;
	}
	Self operator--(int)
	{
		Self tmp(_node);
		_node = _node->_prev;
		return tmp;
	}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	bool operator==(const Self&it)
	{
		return _node == it._node;
	}
	bool operator!=(const Self& it)
	{
		return _node != it._node;
	}
};

template<class T>
class list
{
	typedef ListNode<T> Node;
public:
	typedef ListIterator<T, T&, T*> iterator;
	iterator begin()
	{
		return iterator(_head->_next);
	}
	iterator end()
	{
		return iterator(_head);
	}
	typedef ListIterator<T, const T&,const T*> const_iterator;
	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;
	}
	list()
	{
		empty_init();
	}
	list(const list<T>& il)
	{
		empty_init();
		for (const auto& e : il)
		{
			push_back(e);
		}
	}
	list(initializer_list<T> il)
	{
		empty_init();
		for (const auto& e : il)
		{
			push_back(il);
		}
	}

	list<T>& operator=(const list<T> lt)
	{
		swap(_head, lt._head);
		return *this;
	}
	void clear()
	{
		auto it = begin();
		while (it != end())
		{
			it = erase(it);
		}
	}
	~list()
	{
		clear();
		delete _head;
		_head = nullptr;
	}

	iterator insert(iterator pos, const T& x)
	{
		Node* newnode = new Node(x);
		Node* cur = pos._node;
		Node* prev = cur->_prev;

		newnode->_next = cur;
		newnode->_prev = prev;
		prev->_next = newnode;
		cur->_prev = newnode;
		return iterator(newnode);
	}
	iterator erase(iterator pos)
	{
		Node* cur = pos._node;
		Node* prev = cur->_prev;
		Node* next = cur->_next;

		prev->_next = next;
		next->_prev = prev;
		delete cur;
		return iterator(next);
	}

	void pop_back()
	{
		erase(--end());
	}
	void pop_front()
	{
		erase(begin());
	}

	void push_back(const T& x)
	{
		insert(begin(), x);
	}
	void push_front(const T& x)
	{
		insert(end, x);
	}
private:
	Node* _head;
};

小结

本篇博客介绍了list的使用和模拟实现,通过对list的模拟实现,我们介绍了迭代器失效的问题以及迭代器的封装。

感谢您在百忙之中能够看完本篇文章,如果本篇文章对您有所帮助的话,希望您能留下点赞评论加关注,您的支持就是我创作的最大动力。

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

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

相关文章

go前后端开源项目go-admin,本地启动

https://github.com/go-admin-team/go-admin 教程 1.拉取项目 git clone https://github.com/go-admin-team/go-admin.git 2.更新整理依赖 go mod tidy会整理依赖&#xff0c;下载缺少的包&#xff0c;移除不用的&#xff0c;并更新go.sum。 # 更新整理依赖 go mod tidy 3.编…

go 分布式redis锁的实现方式

go 语言以高并发著称。那么在实际的项目中 经常会用到锁的情况。比如说秒杀抢购等等场景。下面主要介绍 redis 布式锁实现的两种高并发抢购场景。其实 高并发 和 分布式锁 是一个互斥的两个状态&#xff1a; 方式一 setNX&#xff1a; 使用 redis自带的API setNX 来实现。能解决…

深入理解递归:从原理到C++实践

什么是递归&#xff1f; 递归&#xff08;Recursion&#xff09;是编程中一种强大的技术&#xff0c;其核心思想是&#xff1a;函数直接或间接地调用自身。如同俄罗斯套娃一般&#xff0c;每个函数调用都会解开问题的一个层级&#xff0c;直到达到基础条件。 递归三要素&…

MyBatis-Plus 入门详解:从零搭建高效持久层

一、MyBatis-Plus 简介 MyBatis-Plus&#xff08;简称 MP&#xff09;是 MyBatis 的增强工具&#xff0c;在保留 MyBatis 原生功能的基础上&#xff0c;提供了全自动化的 CRUD 操作、强大的分页插件、代码生成器等功能&#xff0c;显著减少开发工作量。与原生 MyBatis 相比&…

阿里云物联网获取设备属性api接口:QueryDevicePropertyData

阿里云物联网接口&#xff1a;QueryDevicePropertyData 说明&#xff1a;调用该接口查询指定设备或数字孪生节点&#xff0c;在指定时间段内&#xff0c;单个属性的数据 比如提取上传到物联网的温度数据 api文档&#xff1a;QueryDevicePropertyData_物联网平台_API文档-阿里…

歌曲分类和流行度预测

1. 项目介绍 本项目从kaggle平台上下载了数据集&#xff0c;该数据集包含了3万多首来自Spotify API 的歌曲&#xff0c;共有23个特征。首先对数据集进行预处理&#xff0c;如重复行、缺失值、标准化处理等。再对预处理后的数据进行探索性分析&#xff0c;观察各变量的分布情况&…

不重启mysql情况下排查慢SQL

查状态 mysql> show variables like %slow_query_log%; 开启慢日志 mysql> set global slow_query_logON; 设置1s超时 mysql> set global long_query_time1; 如果想更小&#xff0c;可以设置0.5 查看慢SQL的日志 cat /var/lib/mysql/localhost-slow.log &…

27、Java 反射机制

15-1 Java 反射机制概述 Reflection&#xff08;反射&#xff09;是被视为动态语言的关键 动态语言&#xff1a;在运行时代码可以根据某些条件改变自身结构。如 C#\JavaScript\PHP 静态语言&#xff1a;运行时结构不可变的语言。如 Java\C\C 问题&#xff1a;通过直接new的方…

Android 端侧运行 LLM 框架 MNN 及其应用

MNN Chat Android App - 基于 MNN 引擎的智能聊天应用 一、MNN 框架简介与工作原理1.1 什么是 MNN&#xff1f;1.2 MNN 的工作原理 二、MNN Chat Android App2.1 MNN Chat 的功能2.2 MNN Chat 的优势2.3 MNN Chat Android App 的使用 三、总结 随着移动端人工智能需求的日益增长…

FPGA学习(一) —— 四位全加器

FPGA学习&#xff08;一&#xff09; —— 四位全加器 文章目录 FPGA学习&#xff08;一&#xff09; —— 四位全加器一、半加器1、半加器的真值表2、Verilog代码实现3、RTL原理图4、波形仿真 二、一位全加器1、一位全加器真值表2、Verilog代码实现3、RTL原理图4、波形仿真 三…

PHP:IDEA开发工具配置XDebug,断点调试

文章目录 一、php.ini配置二、IDEA配置 一、php.ini配置 [xdebug] zend_extension"F:\wamp64\bin\php\php7.4.0\ext\php_xdebug-2.8.0-7.4-vc15-x86_64.dll" xdebug.remote_enable on xdebug.remote_host 127.0.0.1 xdebug.remote_port 9001 xdebug.idekey"…

LINUX网络基础 - 网络编程套接字,UDP与TCP

目录 前言 一. 端口号的认识 1.1 端口号的作用 二. 初识TCP协议和UDP协议 2.1 TCP协议 TCP的特点 使用场景 2.2 UDP协议 UDP的特点 使用场景 2.3 TCP与UDP的对比 2.4 思考 2.5 总结 三. 网络字节序 3.1 网络字节序的介绍 3.2 网络字节序思考 四. socket接口 …

QT实现单个控制点在曲线上的贝塞尔曲线

最终效果: 一共三个文件 main.cpp #include <QApplication> #include "SplineBoard.h" int main(int argc,char** argv) {QApplication a(argc, argv);SplineBoard b;b.setWindowTitle("标准的贝塞尔曲线");b.show();SplineBoard b2(0.0001);b2.sh…

Linux基础开发工具(vim编译器,yum与apt软件安装)

Linux 下载安装软件的方案 源代码安装-》》》非常麻烦与复杂一步错步步错 rmp包安装 -》》》只是安装没有对应的库与依赖相当于只是一个外壳 包管理器进行安装-》》 yum / apt(本篇重点讲解) 1.什么是软件包和软件包管理器 就好⽐ "App" 和 "应⽤商店"…

神经网络 - 激活函数(Maxout 单元)

一、Maxout 单元 Maxout 单元是一种特殊的激活函数&#xff0c;用于神经网络中&#xff0c;其主要思想是通过多个线性变换的最大值来作为神经元的输出&#xff0c;从而提高模型的表达能力和鲁棒性。 1. 数学定义 假设输入为 x&#xff0c;Maxout 单元会计算 k 个线性变换&am…

nginx+keepalived负载均衡及高可用

1 项目背景 keepalived除了能够管理LVS软件外&#xff0c;还可以作为其他服务的高可用解决方案软件。采用nginxkeepalived&#xff0c;它是一个高性能的服务器高可用或者热备解决方案&#xff0c;Keepalived主要来防止服务器单点故障的发生问题&#xff0c;可以通过其与Nginx的…

VirtualBox虚拟机转VM虚拟机

前言&#xff1a;部分靶机只适用于VirtualBox&#xff0c;VM打不开VirtualBox的文件&#xff0c;所以需要进行转换 前置条件&#xff1a;本机已经下载VM和VirtualBox 第一步&#xff1a;文件转换 找到VirtualBox.exe所在位置&#xff0c;启动cmd窗口 文件转换的命令&#xf…

使用DeepSeek+KIMI生成高质量PPT

一、使用DeepSeek DeepSeek官网&#xff1a;DeepSeek 点击“开始对话”&#xff0c;进入交互页面。 在上图中&#xff0c;输入问题&#xff0c;即可获取AI生成的结果。 基础模型&#xff08;V3&#xff09;&#xff1a;通用模型&#xff08;2024.12&#xff09;&#xff0c;高…

基于SpringBoot的失物招领平台的设计与实现

基于SpringBoot的失物招领平台的设计与实现 基于微信小程序的失物招领系统 失物招领小程序 校园失物招领小程序 基于微信小程序SSMMySQL开发&#xff0c;高分JAVA成品毕业设计&#xff0c;附带往届论文、启动教程、讲解视频、二次开发教程和配套安装包文件&#xff0c;论文中…

鸿蒙NEXT开发-元服务和服务卡片的开发

注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注&#xff0c;博主会一直更新鸿蒙next相关知识 目录 1. 元服务基本概念 1.1 基本介绍 1.2 元…