C++初阶 List的模拟实现

news2024/10/7 16:23:44

作者:@小萌新
专栏:@C++初阶
作者介绍:大二学生 希望能和大家一起进步
本篇博客简介:模拟STL中List的实现
在这里插入图片描述

List的模拟实现

  • 本篇博客目标
  • 节点类的实现
    • 构造函数
  • 迭代器类的实现
    • 迭代器类模拟实现的意义
    • 迭代器类的三个参数说明
    • 构造函数
    • ++运算符重载
    • - -运算符的重载
    • ==运算符重载
    • !=运算符重载
    • *运算符重载
    • ->运算符重载
  • List的模拟实现
    • 构造函数
    • begin和end函数
    • front和back函数
    • insert
    • earse
    • push_back和push_front
    • pop_back和pop_front
    • size
    • clear
    • empty
    • swap
    • resize
  • 总结

本篇博客目标

  1. 模拟List类的实现
  2. 模拟迭代器类的实现
  3. 模拟List实现

节点类的实现

List在底层实现的时候实际上就是一个底层带头循环双向链表

结构表示如下

在这里插入图片描述

如果说对于带头双向循环链表没有一个清晰认知的同学可以参考下我的这篇博客

带头循环双向链表

在实现List之前我们首先要实现一个节点类

(因为是自定义类型的数据)

大概的结构表示如下

template<typename T> // Class T
class _list_node
{
public:
	
	_list_node(const T& val = T()) // 默认初始化一个


	T _val;     // 值
	T* _next;   // 指向后面一个node
	T* _prev;   // 指向前面一个node
};

节点类的作用只有一个 创建一个新的类给我们的List

至于释放则不用管 我们List里面会考虑

这就相当于我们用C语言写的一个Buylistnode函数一样

构造函数

	_list_node(const T& val = T()) // 默认初始化一个
		:_val(val) // 默认初始化为0 这里实际上用到了一个匿名函数
		,_next(nullptr)
		,_prev(nullptr)
	{

	}

代码表示如上 到这里我们节点类的所有任务就都完成了

迭代器类的实现

迭代器类模拟实现的意义

回想看我们之前的vector类和String类 我们是不是就直接开始写了啊

迭代器好像就直接typedef一下就可以了那为什么List里面要专门写一个迭代器类呢?

这个就跟这俩数据的结构有关系了

对于Vector或者是String类来说 它们的底层是用使用一块连续的空间 因此通过指针的加加减减就可以得

到后面的数据 因此vector和string中的迭代器就是原生指针

那么我们为什么在list当中要使用迭代器类呢?

因为我们说过了list的数据结构是一个带头双向循环链表 是不连续的 所以我们必须要使用指针的

next prev来找到前后元素

那么我们为什么不直接找 而使用迭代器呢?

这是因为

可以不让用户关心底层实现 用简单统一的方式对容器进行访问

总结下来就是

list中的迭代器类 实际上就是对指针节点进行了封装 对其各种运算符进行了重载 使得节点指针的

各种行为看上去和原生指针一模一样。

(回想看看我们之前的日期类的++)

迭代器类的三个参数说明

template<class T, class Ref, class Ptr>

这里的三个参数的意义分别是 原类型 引用类型 指针类型

至于为什么这么设计 我相信大家在看完所有的代码之后会有一个很好的感悟

框架如下

template<class T, class Ref, class Ptr>
struct _list_iterator
{
	typedef _list_node<T>  node;
	typedef _list_iterator<T, Ref, Ptr>  self;

	node* _pnode; 
};

构造函数

迭代器类实际上就是对节点指针进行了封装 其成员变量只有一个 那就是一个指针 所以我们初始化也只

用初始化这个指针就可以

代码表示如下

//构造函数
_list_iterator(node* pnode)
	:_pnode(pnode)
{}

++运算符重载

前置++操作符本来的意思的是让数据自增 然后返回自增后的数据

但是对于我们这里的迭代器来说++的意思则是让它指向下一个数据

所以说我们应该这么写

代码表示如下

	self operator++()
	{
		_pnode = _pnode->_next;
		return *this;
	}

而为了区分前置++和后置++ 我们在参数内部放置一个占位操作符

	self operator++(int)
	{
		self tmp(*this) //作为返回值
		_pnode = _pnode->_next;
		return tmp;
	}

- -运算符的重载

跟++运算符很类似 我们这里直接给出代码

前置- -

	self operator--()
	{
		_pnode = _pnode->_prev;
		return *this;
	}

后置–


	self operator--(int)
	{
		self tmp(*this); // 作为返回值
		_pnode = _pnode->_prev;
		return *this;
	}

==运算符重载

当我们使用两个==运算符重载的时候 我们实际上是比较的两个迭代器是否是同一个迭代器

那么想想看我们应该怎么比较呢? 看看两个的指针是否是同一个是不是就好了

	bool operator==(const self& it) const
	{
		return it._pnode == _pnode;
	}

!=运算符重载

这里我们直接判断两个指针不同就可以

	bool operator!=(const self& it) const
	{
		return it._pnode != _pnode;
	}

*运算符重载

我们使用解引用运算符的时候 我们希望的是拿到该位置的数据

	ref operator *()
	{
		return _pnode->_val; // 返回指针指向的值
	}

->运算符重载

我们使用解引用运算符的时候我们是想访问我们内部的值

所以说这个时候呢我们需要返回值的地址即可

	Ptr operator ->()
	{
		return &(_pnode->_val);
	}

行文至此 我们应该能够明白为啥要定义三个参数了吧

因为我们要返回不同的三个类型的值

List的模拟实现

构造函数

List的结构是一个带头双向循环链表 所以说我们让头尾指针都指向自己即可

	List()
	{
		_head = new node; //创建一个新的头节点
		_head->_prev = _head; // 头节点的前一个指针指向自身
		_head->_next = _head; // 头节点的后一个指针指向自身
	}

接下来的几个函数都需要用到我们后面的实现的接口函数 所以说我们放到后面再实现

begin和end函数

我们这里要明白 begin返回的是第一个节点(头节点的下一个节点的指针) end返回的是头节点

知道这两个返回的是啥之后我们的代码就好写了

	iterator begin()
	{
		return iterator(_head->_next);
	}

	iterator end()
	{
		return iterator(_head);
	}

当然我们这里还需要重载一个const版本

代码表示如下

	const_iterator begin() const
	{
		return iterator (_head->_next);
	}

	const_iterator end() const
	{
		return iterator(_head);
	}

front和back函数

这两个函数的作用是返回front的back的值

	T& front()
	{
		return *begin();
	}

	T& back()
	{
		return *(--end());
	}

当然啦 还有它们的const版本

	const T& front() const
	{
		return *begin();
	}

	const T& back() const
	{
		return *(--end());
	}

insert

我们这里插入一个新节点

传入两个参数就可以 一个迭代器来表示位置 一个数值来表示我们要插入的数

代码表示如下

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

		//
		cur->_prev = newnode;
		newnode->_next = cur;

		prev->_next = newnode;
		newnode->_prev = prev;
	}

earse

就是一个双链表的删除 保存下头尾节点就好了

代码表示如下

	iterator earse(iterator pos)
	{
		assert(pos != end());

		node* cur = pos._pnode; //迭代器pos处的结点指针
		node* prev = cur->_prev; //迭代器pos前一个位置的结点指针
		node* next = cur->_next; //迭代器pos后一个位置的结点指针

		delete cur; //释放cur结点

		//建立prev与next之间的双向关系
		prev->_next = next;
		next->_prev = prev;

		return iterator(next); //返回所给迭代器pos的下一个迭代器
	}

push_back和push_front

我们这里复用前面的insert就可以啦

尾差就是插入到end迭代器的前面

头插入就是插入到头的下一个节点的前面

代码表示如下

//尾插
void push_back(const T& x)
{
	insert(end(), x); //在头结点前插入结点
}
//头插
void push_front(const T& x)
{
	insert(begin(), x); //在第一个有效结点前插入结点
}

pop_back和pop_front

void pop_back()
{
	erase(--end()); //删除头结点的前一个结点
}

void pop_front()
{
	erase(begin()); //删除第一个有效结点
}

size

因为我们是链表 我们只能逐个统计有效节点的个数只能遍历计数

size_t size() const
{
	size_t sz = 0; //统计有效数据个数
	const_iterator it = begin(); //获取第一个有效数据的迭代器
	while (it != end()) //通过遍历统计有效数据个数
	{
		sz++;
		it++;
	}
	return sz; //返回有效数据个数
}

clear

我们通过遍历的方式 逐个删除即可

代码表示如下

void clear()
{
	iterator it = begin();
	while (it != end()) //逐个删除结点,只保留头结点
	{
		it = erase(it);
	}
}

empty

我们直接看看头尾迭代器是否相同就好

bool empty() const
{
	return begin() == end(); //判断是否只有头结点
}

swap

因为其实里面只有两个指针 所以说我们交换指针之后就完成交换了

void swap(list<T>& lt)
{
	::swap(_head, lt._head); //交换两个容器当中的头指针即可
}

resize

resize的规则如下

1 当给予的数小于我们目前数 我们删除到我们给予的数为止(earse)
2 当给予的数大于我们目前数 我们增加到我们给予的数为止 (push_back)

代码表示如下

void resize(size_t n, const T& val = T())
{
	iterator i = begin(); //获取第一个有效数据的迭代器
	size_t len = 0; //记录当前所遍历的数据个数
	while (len < n&&i != end())
	{
		len++;
		i++;
	}
	if (len == n) //说明容器当中的有效数据个数大于或是等于n
	{
		while (i != end()) //只保留前n个有效数据
		{
			i = erase(i); //每次删除后接收下一个数据的迭代器
		}
	}
	else //说明容器当中的有效数据个数小于n
	{
		while (len < n) //尾插数据为val的结点,直到容器当中的有效数据个数为n
		{
			push_back(val);
			len++;
		}
	}
}

总结

在这里插入图片描述

本篇博客模拟了List的实现
由于博主才疏学浅错误在所难免 如果大佬看到希望能及时指正啊
如果本文帮助到了你别忘了一键三连啊
阿尼亚 哇酷哇酷!

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

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

相关文章

zabbix自定义模板,邮件报警,代理服务器,自动发现与自动添加及snmp

内容预知 1.自定义监控内容 监控案例1&#xff1a;登录人数检测 具体步骤 步骤一&#xff1a;在客户端创建自定义 key 步骤二&#xff1a;在 Web 页面创建自定义监控项模板 &#xff08;1&#xff09;创建模板 &#xff08;2&#xff09;创建应用集&#xff08;用于管理…

《数据结构》顺序表ArrayList

《数据结构》顺序表ArrayList 文章目录《数据结构》顺序表ArrayList什么是顺序表:模拟实现顺序表(以int类型为例)详解ArrayList:ArrayList实现的接口:ArrayList的构造方法:对源码的解析:ArrayList的方法ArrayList的扩容机制ArrayList的缺陷什么是顺序表: 顺序表是用一段物理地…

【Hack The Box】windows练习-- acute

HTB 学习笔记 【Hack The Box】windows练习-- acute &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月17日&#x1f334; &#x1f…

并发入门组件AQS源码解析(未完善)

必要掌握技术 阻塞&#xff0c;唤醒与中断 阻塞与唤醒 LockSupport的park使用的是Parker->park() synchronized的wait&#xff0c;synchronized的monitorenter使用的是ParkEvent->park()&#xff0c; 而LockSupport的unpark&#xff0c;Parker->unpark() synchroni…

员工离职后,账号权限怎么自动化回收?

最近一则离职员工报复前东家的新闻引人注目。起因是该员工被公司辞退后怀恨在心&#xff0c;于是用自己的笔记本电脑入侵了前公司的服务器&#xff0c;进入了该公司的法国站、英国站、德国站三个亚马逊店铺&#xff0c;进行了大幅调低商品价格、主动向客户发起退款、调高广告预…

1525_AURIX TC275 BootROM上

全部学习汇总&#xff1a; GitHub - GreyZhang/g_TC275: happy hacking for TC275! 这一次看一个全新的章节&#xff0c;BootROM&#xff0c;这是我之前只听过但是没有接触过的一个功能。 1. BootROM包含的三个主要的功能&#xff1a;启动软件、引导加载程序、测试固件。 2. 启…

UI设计都有哪些设计原则,分享三个给你

是什么使一个好UI设计容易阅读&#xff1f;是什么让用户轻松浏览&#xff1f;设计师如何创造一个闪亮的UI&#xff1f;任何软件产品的关键部分都是用户界面。 ​好的UI设计&#xff0c;用户甚至会忽略它。如果做得不好&#xff0c;就会成为用户使用产品的绊脚石。为了更有效地设…

数字化车间认定条件

一、申报数字化车间的奖励&#xff1a; 聊城市为了支持企业开展智能制造。对新获认定的国家级智能制造示范工厂、智能制造优秀场景&#xff0c;分别给予最高100万元、50万元一次性奖励&#xff1b;对新获认定的省级智能制造系统解决方案供应商、智能制造标杆企业、智能工厂、数…

因误删文件导致CentOS7开机卡死无法进入图形登录界面

目录 1、背景 2、解决步骤 1、背景 这几天在清理电脑&#xff0c;需要删除虚拟机&#xff0c;为此写下了Linux系统下卸载VMware Workstation软件_nanke_yh的博客-CSDN博客&#xff0c;但是同时怕有残留&#xff0c;自己全局搜索了vm&#xff0c;删除了部分带有vm的文件。删除…

【GridMask】《GridMask Data Augmentation》

arXiv-2020 文章目录1 Background and Motivation2 Related Work3 Advantages / Contributions4 GridMask5 Experiments5.1 Image Classification5.2 Object Detection on COCO Dataset5.3 Semantic Segmentation on Cityscapes5.4 Expand Grid as Regularization6 Conclusion&…

MongoDB之完整入门知识(集合操作、文档基本CRUD操作、文档分页查询、索引等相关命令)

MongoDB完整入门知识一、相关概念1、简介2、体系结构3、安装网址二、MongoDB基本常用命令1、Shell连接&#xff08;mongo命令&#xff09;2、选择和创建数据库2.1 选择和创建数据库的语法格式&#xff08;如果数据库不存在&#xff0c;则自动创建&#xff09;2.2 查看有权限查看…

SpringBoot与Loki的那些事

因为网上好多都没有通过Loki的API自己实现对日志监控系统&#xff0c;所以我就下定决心自己出一版关于loki与springboot的博文供大家参考&#xff0c;这个可以说是比较实用&#xff0c;很适合中小型企业。因此我酝酿了挺久了&#xff0c;对于loki的研究也比较久&#xff0c;希望…

论文精读《OFT: Orthographic Feature Transform for Monocular 3D Object Detection》

OFT: Orthographic Feature Transform for Monocular 3D Object Detection 文章目录OFT: Orthographic Feature Transform for Monocular 3D Object Detection论文精读摘要&#xff08;Abstract&#xff09;1. 介绍&#xff08;Introduction&#xff09;2. 相关工作&#xff08…

给开源项目做一个漂亮简洁的版本迭代更新图,生成固定链接复制到介绍中、公众号菜单链接中、博客中等

背景 开源项目的版本迭代与更新经常需要更新迭代文档&#xff0c;但是readme.md没有比较美观一点的效果&#xff0c;所以文本分享一种第三方的方式&#xff1a;用TexSpire的免费在线文档分享功能&#xff0c;手机、PC、Pad都可以适配。 效果预览 使用 第一步&#xff1a;创…

浅谈 async/await 和生成器

浅谈 async/await async/await 是ES8规范新增的&#xff0c;使得以同步方式写的代码异步运行不再是白日梦&#xff0c;进一步让代码逻辑更加清晰。 为什么新增 async/await 下面有这样一个需求&#xff1a;有两个请求&#xff0c;请求 1 的结果是请求 2 的参数&#xff0c;所…

机器学习6——EM算法与高斯混合模型GMM

前置内容 Jensen不等式 高斯混合模型 多元高斯模型 拉格朗日乘子法 主要内容 EM算法&#xff08;Expectation-Maximization&#xff09;&#xff0c;期望-最大化。 用于保证收敛到MLE&#xff08;最大似然估计&#xff09;。主要用于求解包含隐变量的混合模型&#xff0c;主要…

R生成三线表

R生成三线表table1包测试数据生成制作三线表Tableone包加载包 探索数据类型数据整理分类构建Table函数Tableone函数细节主要基于table1 或tableone 包table1包 测试数据生成 datadata.frame(性别sample(c("男","女"), 1000,replaceT),年龄round(rnorm(10…

2021年认证杯SPSSPRO杯数学建模A题(第一阶段)医学图像的配准全过程文档及程序

2021年认证杯SPSSPRO杯数学建模 A题 医学图像的配准 原题再现&#xff1a; 图像的配准是图像处理领域中的一个典型问题和技术难点&#xff0c;其目的在于比较或融合同一对象在不同条件下获取的图像。例如为了更好地综合多种信息来辨识不同组织或病变&#xff0c;医生可能使用…

5年自动化测试,终于进字节跳动了,年薪30w其实也并非触不可及

一些碎碎念 什么都做了&#xff0c;和什么都没做其实是一样的&#xff0c;走出“瞎忙活”的安乐窝&#xff0c;才是避开弯路的最佳路径。希望我的经历能帮助到有需要的朋友。 在测试行业已经混了5个年头了&#xff0c;以前经常听到开发对我说&#xff0c;天天的点点点有意思没…

java计算机毕业设计springboot+vue+elementUI永加乡精准扶贫信息管理系统

项目介绍 系统设计的主要意义在于&#xff0c;一方面&#xff0c;对于网站来讲&#xff0c;系统上线后可以带来很大的便利性&#xff0c;精准扶贫网站管理属于非常细致的管理模式&#xff0c;要求数据量大&#xff0c;计算机管理可以提高精确性&#xff0c;更为便利的就是信息…