learn C++ NO.13——list

news2024/12/23 10:32:00

前言

本文将从list的使用,再到根据sgi库对于list实现作为参考模拟实现一下list。通过模拟实现来增加对它的理解。

介绍list

list是一个由带头双向循环链表实现的STL容器,它提供常规时间内对数据进行插入和删除操作。
list在内存中存储不连续的空间存储,这样避免了连续存储的扩容问题。
list支持双向迭代器,即支持从前往后遍历容器和从后往前遍历容器。

list的使用

下面写一份简单的代码介绍一下list的基本接口。
在这里插入图片描述

在insert接口和erase接口的使用中,list相较于vector来说用起来有些区别。
vector调用insert()接口要在第二个元素位置之后,可以直接使用迭代器+2作为参数传递。而list不可以。list想要做到在第二个元素之后插入的话,需要先将迭代器移动到指定位置后,在插入数据。

int main()
{
	vector<int> v(4,0);
	list<int> lt(4,0);
	
	// 在第二个元素后插入一个值
	v.insert(v.begin() + 2, 10);
	
	auto ite = lt.begin();
	int n = 2
	while(n--)
	{
		++ite;
	}

	lt.insert(ite, 10);
	return 0;
}

为什么list不像vector一样能直接将迭代器+2传参呢?我们知道vector存储在一段连续的空间中,而list不一定是存储在一段连续的空间中。从技术实现的角度来说,list使用迭代器+2找到第二个元素后的位置是肯定可以实现的。至于为什么不这么做呢?我个人看来,可能是标准委员会的大佬们觉得list头尾插入删除效率极佳,如果支持的话有些使用者不了解底层的实现原理,势必不了解list效率上的不足,这样一来可以引导程序员在大量数据删除、插入操作在容器中间位置时,去用vector存储数据以便操作。

既然谈到了vector和list存储特点上有区别,下面引申出一些关于迭代器为什么不实现了operator < 、operator >等,而是统一用operator!=或operator==做条件判断的操作符。因为list、map、set等容器都不是存储在一段连续的空间上,所以,并不意味着第一个元素的地址就在第二个元素的地址前面。迭代器这样实现上为了兼容非线性存储结构的容器也能正常的使用迭代器进行操作。

list迭代器失效问题

vector的insert()和erase()后都会导致迭代器失效问题,因为vector的insert()涉及到扩容操作会导致迭代器失效,以及erase接口删除特定位置元素后导致的迭代器失效问题。那么list会存在迭代器失效问题吗?答案是会。但是,insert接口不会涉及迭代器失效问题。list底层实现不涉及扩容操作,所也就不存在迭代器失效了。
在这里插入图片描述

而erase接口依旧会导致迭代器失效问题。
在这里插入图片描述

简介迭代器

STL库对于STL容器的迭代器实现分为三类,一种是单向迭代器(forward iterator),单链表(forward_list)、哈希表(unordered_map/unordered_set)使用的就是单向迭代器,单项迭代器只能从前往后进行迭代,所以只提供了operator++重载。另一种是双向迭代器(bidirectional iterator ),双向链表(list)、红黑树系列(map/set)所使用的是双向迭代器。双向迭代器提供了operator++、operator–。还有一种是随机迭代器(random access iterator )。vector、deque、string都是使用的随机迭代器。相较于双向迭代器,随机迭代器支持了对于迭代器的operator+、operator-的重载和operator[]的重载。

聊一聊list提供的操作接口

由于list并不支持随机迭代器,所以算法库中的sort是无法调用的,因为底层使用快速排序实现的,必须使用随机迭代器才能调用。而list库中提供的sort底层是用归并排序实现的。list的sort相比于算法库里的sort其实效率是拉胯的。
在百万级以上的数据排序下,list的sort甚至不如将list的数据拷贝到vector排序,再拷贝回list。下面做一组对照实验带大家看看。我们的两组对照组,排序数据是1000w个随机值,一组我们使用list库的sort,与另一个测试组,现将数据拷贝到vector后,再调算法库的sort,最后再拷贝回list。通过比对两者时间差距便知。

在这里插入图片描述

可以看到在1000w个数据的量级下,list排序和拷贝到vector排序后再拷回来的时间差距是一个接近二倍的差距。

list的sort,在数据量小的情况下其实使用起来问题不大。提供sort接口更多是为了可以配合以下的接口进行使用。merge()函数,用于归并两个有序地链表。unique函数,用于有序链表的去重操作。

归并两个有序链表的操作

在这里插入图片描述

对有序链表去重
在这里插入图片描述
reverse()逆置链表操作
在这里插入图片描述

remove()删除指定值的元素

在这里插入图片描述

splice()转移链表到指定位置。
在这里插入图片描述

模拟实现list

首先我们搭一个最简单的框架,然后再慢慢完善它。
在这里插入图片描述

首先是先把类给定义出来,这里不仅需要定义list类,还需要定义一个描述节点的类。
在这里插入图片描述
在这里插入图片描述
push_back()接口实现思路这里简单说一下,我们先定义一个局部变量tail存一下原链表的尾,以方便尾插。然后就是创建新节点。让新节点的_prev指向tail,让tail的_next指向新节点。让_head节点的_next结点指向新节点,最后让新节点的_prev指向_head即可。

模拟实现迭代器

由于list的迭代器需要重载operator++、operator–、operator!=、operator==以及operator* 等运算符重载。其中operator*是访问节点的数据而不是获取节点。我们需要用自定义类型来封装list的迭代器。这与string、vector等连续空间存储的容器不同,它们的迭代器可以是原生指针来定义的(vs用的是结构体定义自定义类型封装)。而list它是非连续空间存储。所以迭代器实现采用自定义类型封装。

template<class T>
struct __list_iterator
{
	typedef list_node<T> Node;

	//成员变量
	Node* _node;

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

	__list_iterator<T>& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	__list_iterator<T>& operator++(int)
	{
		__list_iterator<T> tmp(*this);
		_node = _node->_next;
		return tmp;
	}

	__list_iterator<T>& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	__list_iterator<T>& operator--(int)
	{
		__list_iterator<T> tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

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

	bool operator!=(const __list_iterator<T>& it)
	{
		return _node != it._node;
	}


	bool operator==(const __list_iterator<T>& it)
	{
		return _node == it._node;
	}
};

这里我们就实现了一份简易的迭代器,下面我们简单测试一下。
在这里插入图片描述

那const迭代器要如何实现呢?直接定义吗?答案是不是的。list的const迭代器是为了保证指向的数据不被修改,但是迭代器本身还是要支持修改的。所以我们可以再上面的基础上把我们的operator*的参数修改一下即可。

template<class T>
struct __list_const_iterator
{
	typedef list_node<T> Node;

	//成员变量
	Node* _node;

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

	__list_const_iterator<T>& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	__list_const_iterator<T>& operator++(int)
	{
		__list_const_iterator<T> tmp(*this);
		_node = _node->_next;
		return tmp;
	}

	__list_const_iterator<T>& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	__list_const_iterator<T>& operator--(int)
	{
		__list_const_iterator tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

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

	bool operator!=(const __list_const_iterator<T>& it)
	{
		return _node != it._node;
	}


	bool operator==(const __list_const_iterator<T>& it)
	{
		return _node == it._node;
	}
};

那这样设计是不是太冗余了,我们这时候可以参看一下SGI库大佬实现的思路了。多加两个模板参数,分别是Ref(用作operator*的返回值类型)以及Ptr(用作operator->的返回值类型)。这样我们就不用实现两份代码了,进需要控制模板的参数即可完成const迭代器和普通迭代器的实现了。

//typedef __list_iterator<T, T&, T*> iterator;
//typedef __list_iterator<T, const T&, const T*> const_iterator;

template<class T, class Ref, class Ptr>
struct __list_iterator
{
	typedef list_node<T> Node;
	typedef __list_iterator<T,Ref,Ptr> Self;
	//成员变量
	Node* _node;

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

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

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

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

	Self operator--(int)
	{
		Self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

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

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

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


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

补充聊一下编译器对于operator->调用时的特殊处理。
在这里插入图片描述
其实按照c语言的语法来看我们使用迭代器访问A类型的两个成员变量时,应该是it->->_a1。但是,由于这样写实在是太过于复杂,甚至还不如不实现这个operator->。于是c++委员会规定编译器可以对这个做一个特殊处理,省略一个->,以保证代码的可读性。

erase()和insert()的模拟实现

erase的实现思路如下,断言判断一下迭代器的有效性。定义两个变量保存前后的节点。修改前后节点的指向,然后释放节点,最后返回当前位置的下一个位置的迭代器即可。由于我们定义了成员变量来记录size的值,所以还需要注意修改。

在这里插入图片描述

向pop_back()、pop_front()这类接口复用erase即可。

insert()实现思路如下,定义两个变量保存前后节点,new一个新节点。修改前后节点的指向后,修改一下size的值,最后返回新节点。
在这里插入图片描述

clear()与析构函数的模拟实现

实现clear接口思路如下,遍历一遍链表依次erase即可,最后清空size的值即可。

析构函数实现思路如下只需要先调用clear()释放所有的有效节点,最后delete哨兵位头结点即可。
在这里插入图片描述

拷贝构造实现

我们需要new一个头结点并初始化它,随后我们可以依次将被拷贝对象尾插到头结点后。
在这里插入图片描述

operator=的模拟实现

使用现代写法实现,因为在operator=的形参是实参的临时拷贝,将它的内容交换给成员变量就可以达到赋值的效果。
在这里插入图片描述

总结

本文重点在于list的迭代器部分的实现,通过三个模板参数实现一份迭代器,不仅仅加深了我们对于泛型编程的理解,还加深了我们对于STL容器如何做到操作一致性的原理以及背后设计思路的理解。不得不感慨语言的发展真的是一个不亚于特破某个实体领域的技术壁垒。

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

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

相关文章

Kamailio-超强dispatcher负载均衡模块

Kamailio 负载均衡的功能主要依靠 Dispatcher 模块完成&#xff0c;模块官方文档参看 为什么要引入负载均衡&#xff1f;如何使用&#xff1f; 引入和配置功能路由调用命令行指令 为什么要引入负载均衡&#xff1f; Q: 如果单台VOIP服务的性能不能满足业务需求了&#xff0…

掌握ZooKeeper的二阶段提交及其优缺点

1. ZooKeeper的协议 1.1 ZAB协议 要深入学习ZooKeeper前&#xff0c;胡广认为我们要先学习ZooKeeper的核心理念&#xff0c;所有的ZooKeeper行为都是围绕这个核心来进行的。说了那么多&#xff0c;它就是——ZAB协议。 ZAB协议英文全称叫ZooKeeper Atomic Broadcast&#xf…

TIDB的整体架构和主要功能

1. 基础架构 PD &#xff1a;负责集群管理和调度。TiDB Server &#xff1a;负责 SQL 查询处理。TiKV/TiFlash&#xff1a;负责数据存储和事务处理。 1.1 PD (Placement Driver) Server 1.1.1 基础介绍 整个 TiDB 集群的元信息管理模块&#xff0c;负责存储每个 TiKV 节点实时…

让我们聊一下小团队也可以用的敏捷开发

使用敏捷开发的团队往往需要寻找一个更佳的平衡点&#xff1a; 较少的团队成员&#xff1a;通常更容易沟通和协作&#xff0c;减少了协调成本。小团队&#xff08;如 5 到 9 人&#xff09;能够更灵活地适应变化&#xff0c;且管理和决策过程较为高效。 较多的团队成员&#x…

喜报 | 知从科技荣获 “AutoSec 安全之星 - 优秀汽车软件供应链安全方案奖”

近日&#xff0c;「AutoSec 2024第八届中国汽车网络安全周暨第五届智能汽车数据安全展」在上海盛大举行。本届大会由谈思实验室和谈思汽车主办、上海市车联网协会联合主办&#xff0c;以汽车“网络数据安全、软件安全、功能安全”为主题&#xff0c;设置了“31X”模式&#xff…

Docker学习笔记-部署MySQL-命令解读

部署MySQL 先停掉虚拟机中的MySQL&#xff0c;确保你的虚拟机已经安装Docker&#xff0c;且网络开通的情况下&#xff0c;在MobaXterm中执行下面命令即可安装MySQL。 docker run -d \--name mysql \-p 3306:3306 \-e TZAsia/Shanghai \-e MYSQL_ROOT_PASSWORD123456 \mysql …

c++修炼之路之AVL树与红黑树

目录 一&#xff1a;AVL树 1.AVL树的概念 2.AVL树插入数据后平衡因子及更新的情况 3.AVL树节点的定义 4.AVL树的插入及旋转 二&#xff1a;红黑树 1.红黑树的概念及性质 2.红黑树节点的定义 3.红黑树的插入操作情况 4.红黑树与AVL树的比较 接下来的日子会顺顺利利…

多方位实测运动耳机排行榜前十名,助你选出靠谱的运动耳机!

非常荣幸能与各位运动爱好者共聚本次的骨传导耳机知识分享&#xff01;作为一名深耕运动科技领域多年的专家&#xff0c;今天将主要跟大家分析一下市面上比较热门的骨传导耳机。骨传导耳机作为当下市面上非常新颖且有创意的耳机种类&#xff0c;相信有很多用户都想入手一款&…

流片为啥那么重要?

很多微电子与集成电路专业的学生、初入IC职场的工程师&#xff0c;以及电子/机械大类专业的同学&#xff0c;在进入芯片设计行业时&#xff0c;都或多或少听说了参与流片的重要性。 但是却并不是很清楚——流片到底有多重要&#xff1f;流片为什么重要&#xff1f; 研0的同学…

从基础到进阶:利用EasyCVR安防视频汇聚平台实现高效视频监控系统的五步走

随着科技的飞速发展&#xff0c;视频监控技术在社会安全、企业管理、智慧城市构建等领域扮演着越来越重要的角色。一个高效智能的视频监控管理系统不仅能够提升监控效率&#xff0c;还能在预防犯罪、事故预警、数据分析等方面发挥巨大作用。 一、需求分析 在设计视频监控管理…

存在分包的微信小程序解包反编译还原(含报错处理与代码修复)

01前言 本文主要对微信小程序的解包步骤进行复现梳理&#xff0c;网上虽然已有明确详细的文章&#xff0c;但是实际复现过程中程序报错的情况并不少见。对此情况进行了梳理以及对相关工具的代码、调用方式等进行了优化修复。 本文内容&#xff1a; 常规微信小程序逆向解析的…

el-input设置后缀显示单位并阻止滚轮微调

项目中收集form表单信息时&#xff0c;有时会需要在el-input后面显示单位&#xff0c;效果如图&#xff1a; 当然&#xff0c;我们可以直接在输入框后面加上单位&#xff0c;但直接给输入框上加单位不管是视图上还是用户体验上看起来都要好一点 element-plus / element-ui给我…

MySQL数据库 — Explain命令

EXPLAIN 命令在 MySQL 查询优化中发挥了重要作用。通过 EXPLAIN 的输出&#xff0c;可以获取有关查询执行计划的详细信息&#xff0c;从而有助于优化和调试查询。不过&#xff0c;它也有一定的局限性。 使用Explain EXPLAIN 语句通过在查询前加上 EXPLAIN 关键字来展示查询的…

正则表达式三板斧

推荐练习网站&#xff1a;https://regex101.com/ 解释一下: 1、最常用的就是[]&#xff0c;表示匹配任意字符&#xff0c;[]中所有的变量只需要输入一次&#xff08;比如搜索三个点…&#xff0c;只需要输入[.]即可&#xff09; 2、*表示>0次&#xff0c;表示>1次&#x…

计算机毕业设计选题推荐-推拿知识互动平台-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

大白话!解析大模型原理!

LLM的工作原理对大多数人来说是个谜。虽然它们本质上在于“预测下一个词”&#xff0c;并需要大量文本进行训练&#xff0c;但具体细节往往令人困惑。原因在于这些系统独特的开发方式&#xff1a;**基于数十亿词汇训练的神经网络&#xff0c;不同于传统的人类编写的软件。**尽管…

SpringCache源码解析(三)——@EnableCaching

一、源码阅读 让我们进行源码阅读把。 1.1 阅读源码基础&#xff1a; Import(xxx.class)里的类可以有两种类&#xff1a; ImportSelector接口的实现类&#xff1b;ImportBeanDefinitionRegistrar接口的实现类&#xff1b; 两种接口简介&#xff1a; ImportSelector接口&am…

如何在算家云搭建Open-Sora1.0

一、模型介绍 2024 年 3 月 18 日&#xff0c;Colossal-AI 团队发布了 Open-Sora 1.0 项目&#xff0c;该项目是一个全面开源的视频生成模型项目&#xff0c;项目旨在高效制作高质量视频&#xff0c;并使所有人都能使用其模型、工具和内容。 模型架构 &#xff1a; Open-Sor…

ubuntu20.04 编译vtk 9.3.1+vtkDicom+GDCM 3.0.24

1 下载vtk源码 链接地址如下&#xff1a; Download | VTK 使用cmake-gui编译&#xff08;如何安装使用&#xff0c;查看前两篇文章&#xff09;&#xff0c;运行命令&#xff1a; cmake-gui 如下图所示&#xff0c;选择源码目录和build目录&#xff1a; 勾选 BUILD_SHARED…

基于SpringBoot+Vue+MySQL的志愿服务管理系统

系统展示 用户前台界面 管理员后台界面 系统背景 随着社会对志愿服务需求的日益增长&#xff0c;传统的志愿服务管理方式已难以满足高效、透明、精准的管理需求。为提升志愿服务组织的运营效率&#xff0c;优化资源配置&#xff0c;增强志愿者参与度和满意度&#xff0c;开发基…