Cpp::STL—list类的模拟实现(上)(13)

文章目录

  • 前言
  • 一、结点类的实现
  • 二、迭代器类的实现
    • 迭代器类的存在意义
    • 迭代器类的模板参数
    • 构造函数
    • ++运算符的重载
    • --运算符的重载
    • ==、!=运算符的重载
    • *运算符的重载
    • ->运算符的重载
  • 总结


前言

  注意本篇难度偏高,其主要体现在迭代器类的实现
  什么,list类的迭代器还要单独封装成类!?

  还真是,毕竟它的元素存储在物理意义上不是连续的
  正文开始!


一、结点类的实现

  但是首先我们得先来实现一下节点类,因为我们说list底层是个链表,其实更准确地说是双向链表
在这里插入图片描述
而实现一个结点类。而一个结点需要存储的信息有:数据、前一个结点的地址、后一个结点的地址,于是该结点类的成员变量也就出来了(数据、前驱指针、后继指针)

而对于该结点类的成员函数来说,我们只需实现一个构造函数即可。因为该结点类只需要根据数据来构造一个结点即可,而结点的释放则由list的析构函数来完成

// List的节点类
// 直接设置为公开访问即可,后面很明显有访问成员变量的必要
	template<class T>
	struct ListNode
	{
	// 若构造结点时未传入数据,则默认以list容器所存储类型的默认构造函数所构造出来的值为传入数据
	// 对内置类型和自定义类型都是如此
		ListNode(const T& val = T())
			: _prev(nullptr)
			, _next(nullptr)
			, _val(val)
		{}

		ListNode<T>* _prev;
		ListNode<T>* _next;
		T _val;
	};

二、迭代器类的实现

来了来了

在这里插入图片描述
  这是我们要实现的三个类,而迭代器类的实现基础就是我们刚才实现的节点类
  你可能注意到我们是把迭代器类给公开的,数据公开意味着,内部数据可以被任意修改。但是在这里没人会去跳过封装,使用内部的数据,没有意义。因为不同编译器中底层实现是不一样的(实现逻辑、名称),这本身就是一种隐式设置为私有的作用

迭代器类的存在意义

之前模拟实现string和vector时都没有说要实现一个迭代器类,为什么实现list的时候就需要实现一个迭代器类了呢?其实就像前言说的,因为string和vector对象都将其数据存储在了一块连续的内存空间,我们通过指针进行自增、自减以及解引用等操作,就可以对相应位置的数据进行一系列操作,因此string和vector当中的迭代器就是原生指针,typedef一下就行
在这里插入图片描述
但是对于list来说,其各个结点在内存当中的位置是随机的,并不是连续的,我们不能仅通过结点指针的自增、自减以及解引用等操作对相应结点的数据进行操作,因为链表的各个元素只在逻辑上连续
在这里插入图片描述
而迭代器的意义就是,让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问,我们也想有个list迭代器,可以简单的++、- -、*,理解成本低,所以,封装节点指针,重载运算符就是我们要做的工作

比如说++,我们重载的时候,其实就是在内部让p = p -> _next;只是运用的时候没必要知道而已

迭代器类的模板参数

	template<class T, class Ref, class Ptr>

你可能会感到诧异,为什么迭代器类会有三个模板参数?其实,迭代器分为两种,普通迭代器和const迭代器,我们在list类的模拟实现中会运用到这两个

	typedef ListIterator<T, T&, T*> iterator;
	typedef ListIterator<T, const T&, const T&> const_iterator;

这体现了一种封装智慧,因为两种迭代器的自增自减等等几乎没区别,区别就仅仅在于元素的访问和获取指向元素的指针这两个成员函数的返回值上(比如说元素的访问,一个是T&,一个是const T&),如果是仅仅因为这点小区别,我们就写两个类,那这代码就不算好,于是我们选择丢给编译器,让它实例化的时候帮我们自动生成,尽管这也是实现两个类,但是我们少写了一个

好风凭借力,请说谢谢编译器先生
还是那句话,没有什么岁月静好,都是有人在帮你负重前行

在这里插入图片描述
所以Ref和Ptr分别代表的其实是引用类型和指针类型

构造函数

迭代器类实际上就是对结点指针进行了封装,其成员变量就只有一个,那就是结点指针,其构造函数直接根据所给结点指针构造一个迭代器对象即可

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

++运算符的重载

前置++原本的作用是将数据自增,然后返回自增后的数据。我们的目的是让结点指针的行为看起来更像普通指针,那么对于结点指针的前置++,我们就应该先让结点指针指向后一个结点,然后再返回“自增”后的结点指针即可

而对于后置++,我们则应该先记录当前结点指针的指向,然后让结点指针指向后一个结点,最后返回“自增”前的结点指针即可

	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}
	
	//有拷贝构造就需要考虑深浅拷贝的问题。
	//这里需要使用到浅拷贝,指向同一块空间,并且不需要考虑重复析构的问题,也说明了浅拷贝并都是坏处。
	//临时对象tmp同指向一块空间,调用完临时对象被销毁,指向空间资源保留
	//这也导致了返回类型是指针还是引用
	Self operator++(int)
	{
		Self temp(*this);
		_node = _node->_next;
		return temp;
	}

typedef其实可以将一个较长的类型变短,这里的Self其实就是迭代器类自己
typedef ListIterator< class T, class Ref, class Ptr> Self;

–运算符的重载

对于前置- -,我们应该先让结点指针指向前一个结点,然后再返回“自减”后的结点指针即可

而对于后置- -,我们则应该先记录当前结点指针的指向,然后让结点指针指向前一个结点,最后返回“自减”前的结点指针即可

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

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

==、!=运算符的重载

当使用==、!=运算符比较两个迭代器时,我们实际上想知道的是这两个迭代器是否是同一个位置的迭代器,也就是说,我们判断这两个迭代器当中的结点指针的指向是否相同即可

	// 迭代器间的比较并不是比较指向数据,而是比较迭代器指向的位置
	bool operator!=(const Self& it) const
	{ 
		return _node == it._node;
	}

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

*运算符的重载

当我们使用解引用操作符时,是想得到该位置的数据内容。因此,我们直接返回当前结点指针所指结点的数据即可,但是这里需要使用引用返回,因为解引用后可能需要对数据进行修改

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

->运算符的重载

我们使用迭代器的时候可能会用到->运算符,一般发生在T也是自定义类型的时候

int main()
{
	list<Date> lt;
	
	Date d1(2021, 8, 10);
	Date d2(1980, 4, 3);
	Date d3(1931, 6, 29);
	
	lt.push_back(d1);
	lt.push_back(d2);
	lt.push_back(d3);
	
	list<Date>::iterator pos = lt.begin();
	cout << pos->_year << endl; //输出第一个日期的年份,此时Date的成员变量为公有
	
	return 0;	
}

这个时候我们只需要返回节点所存储数据的指针即可:

	Ptr operator->() 
	{ 
		return &(operator*()); // 复用,毕竟*就是拿元素,&又是取地址符
	}

可是你可能会感觉不对,按道理来说,这种情况不是应该两个->吗?怎么pos->_year就拿到年份这一变量了?
其实这还是编译器弄的鬼,因为假如一个地方出现两个箭头,程序的可读性太差了,所以编译器做了特殊识别处理,为了增加程序的可读性,省略了一个箭头

所以说pos->_year,其实本质上是pos->->_year,更准确的说是pos.operator->()->_year

第一个箭头是pos ->去调用重载的operator->返回Date* 的指针,第二个箭头是Date* 的指针去访问对象当中的成员变量_year
那能不能自己显式写出两个->呢?你自己试试,pos->->_year不行;pos.operator->()->_year可以

在这里插入图片描述


总结

  本篇暂时就先介绍两个类,剩下最后一个list类我们下篇再介绍
  铠甲要合体了!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QD1-P8 HTML 格式化标签(font、pre、b、strong、i、u、del、s、sub、sup)

本节学习&#xff1a;HTML 格式化标签。 本节视频 www.bilibili.com/video/BV1n64y1U7oj?p8 ‍ 一、font 标签 用途&#xff1a;定义文本的字体大小、颜色和 face&#xff08;字体类型&#xff09;。 示例 <!DOCTYPE html> <html><head><meta cha…

JAVA-数据结构-排序

1.直接插入排序 1.原理&#xff1a;和玩扑克牌一样&#xff0c;从左边第二个牌开始&#xff0c;选中这个&#xff0c;和前面的所有牌比较&#xff0c;插在合适的位置 public static void insertsort(int[] arr){//直接插入排序for (int i 1; i < arr.length; i) {//此循环…

LOID:有效提升遮挡条件下的车道检测精度

1.论文信息 论文标题&#xff1a;LOID: Lane Occlusion Inpainting and Detection for Enhanced Autonomous Driving Systems 作者&#xff1a;Aayush Agrawal, Ashmitha Jaysi Sivakumar, Ibrahim Kaif∗, Chayan Banerjee† 作者单位&#xff1a;印度马德拉斯印度理工学院&…

Web安全 - 路径穿越(Path Traversal)

文章目录 OWASP 2023 TOP 10导图定义路径穿越的原理常见攻击目标防御措施输入验证和清理避免直接拼接用户输入最小化权限日志监控 ExampleCode漏洞代码&#xff1a;路径穿越攻击案例漏洞说明修复后的安全代码代码分析 其他不同文件系统下的路径穿越特性Windows系统类Unix系统&a…

【C++】基于红黑树封装set和map

&#x1f680;个人主页&#xff1a;小羊 &#x1f680;所属专栏&#xff1a;C 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 前言一、更高维度的泛型二、模版参数三、比较逻辑的重写四、迭代器4.1 const迭代器4.2 重载4.3 - -重载 五、完整代…

为什么很多人宁愿加钱买港版,也不愿买国行 iPhone 16

最近的 iPhone 16 市场&#xff0c;真的是倒反天罡&#xff0c;攻守异形啊。 过去&#xff0c;港版 iPhone 都是性价比的次选&#xff0c;便宜个 10% 都得考虑考虑。但今年&#xff0c;港版 iPhone 16 的价格&#xff0c;反而比国行还贵。 比如&#xff0c;闲鱼上某个卖家&am…

[红队apt]文件捆绑攻击流程

免责声明:本文用于了解攻击者攻击手法&#xff0c;切勿用于不法用途 前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文整理黑客通过文件捆绑进行攻击的流程思路 文件捆绑原理 废话只多说这一句。 1.exe和2.exe被你捆绑为3.exe。 那么你点击了3.exe就等于点…

kafka消息队列核心内容及常见问题

目录 1. 使用消息队列的目的&#xff08;优点与缺点&#xff09; 2. 常见各消息队列对比 3. kafka介绍 3.1 kafka简介 3.2 kafka特点 3.3 kafka系统架构 3.4 设置数据可靠性 3.4.1 Topic 分区副本 3.4.2 消息确认机制 4. 常见问题&#xff08;面试题&#xff09; 4.…

Acwing 排序

1.快速排序 主要思想&#xff1a;基于分治思想。通过选择一个基准元素&#xff0c;将数组分为两部分&#xff0c;左边部分元素都小于等于基准&#xff0c;右边部分元素都大于等于基准。然后对这两部分分别递归地进行排序。 分区逻辑&#xff1a;双指针算法 左指针i从左往右找…

《RabbitMQ篇》消息应答和发布确认

消息应答 消息应答机制&#xff1a;消费者接收信息并处理完之后&#xff0c;告诉rabbitmq该信息已经处理&#xff0c;rabbitmq可以把该信息删除了. 消息自动重新入队&#xff1a;如果处理某个消息的消费者异常关闭了&#xff0c;没有发送ACK确认&#xff0c;rabbitmq会将其重…

金九银十软件测试面试题(800道)

今年你的目标是拿下大厂offer&#xff1f;还是多少万年薪&#xff1f;其实这些都离不开日积月累的过程。 为此我特意整理出一份&#xff08;超详细笔记/面试题&#xff09;它几乎涵盖了所有的测试开发技术栈&#xff0c;非常珍贵&#xff0c;人手一份 肝完进大厂 妥妥的&#…

postgresql 安装

一、下载 PostgreSQL: File Browser 下载地址 PostgreSQL: File Browser 上传到服务器,并解压 二、安装依赖 yum install -y perl-ExtUtils-Embed readline-devel zlib-devel pam-devel libxml2-devel libxslt-devel openldap-devel 创建postgresql 和目录 useradd …

Java-基础

1. 导入模块不能纯粹的复制粘贴&#xff0c;要从new里导入&#xff0c;因为前者建立不了关联 2. 数组 String[] name{"张三","李四","王五"};int[] numsnew int[]{1,2,3};//二维String[][] names{{"张三","李四"},{"…

39 C 语言枚举类型、枚举常量、枚举变量、枚举的遍历、枚举数组、枚举与 switch

目录 1 什么是枚举 2 定义枚举类型 2.1 语法格式 2.2 枚举元素的特点 2.3 案例演示 3 枚举变量 3.1 什么是枚举变量 3.2 定义枚举变量的多种方式 3.3 案例演示 1&#xff1a;标准版枚举类型 3.4 案例演示 2&#xff1a;简化版枚举类型 3.5 案例演示 3&#xff1a;匿…

uniapp学习(005-2 详解Part.2)

零基础入门uniapp Vue3组合式API版本到咸虾米壁纸项目实战&#xff0c;开发打包微信小程序、抖音小程序、H5、安卓APP客户端等 总时长 23:40:00 共116P 此文章包含第41p-第p51的内容 文章目录 mainifest.json文件配置获取微信小程序appid注册微信小程序微信小程序控制台图形界…

22. 括号生成【回溯】

文章目录 22. 括号生成解题思路Go代码 22. 括号生成 22. 括号生成 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;["((()))",&quo…

node.js服务器基础

node.js的事件循环 node.js是基于事件驱动的&#xff0c;通常在代码中注册想要等待的事件&#xff0c;设定好回调函数&#xff0c;当事件触发的时候就会调用回调函数。如果node.js没有要处理的事件了&#xff0c;那整个就结束了;事件里面可以继续插入事件&#xff0c;如果有事…

手撕数据结构 —— 带头双向循环链表(C语言讲解)

目录 0.前言 1.什么是带头双向循环链表 理解带头 ​编辑 理解双向 理解循环 2.带头双向循环链表的实现 List.h文件中接口总览 具体实现 结点的定义 申请结点 初始化 打印链表 尾插 尾删 头插 头删 ​编辑​编辑 获取大小 查找 在指定位置前插入 ​编辑…

数据结构--线性表双向链表的实现

目录 思路设计 总体思维导图 插入部分 头插法尾插法 任意位置插入 删除部分 头结点 尾节点 中间节点 只有头结点且删除的就是头结点 ​编辑 清空链表部分 遍历清空链表的所有节点 不遍历清空 各部分代码 Main部分 MyListedList部分 IndexOutOfException部分 …

YOLO11改进 | 注意力机制 | 结合静态和动态上下文信息的注意力机制

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 上下文Transformer&#xff08;CoT&…