从list的模拟实现中了解迭代器的设计方式

欢迎来到博主的专栏:c++杂谈
博主ID:代码小豪


文章目录

    • 迭代器——容器与算法的桥梁
    • 容器与迭代器
    • 算法与迭代器
    • 迭代器
    • 总结

迭代器——容器与算法的桥梁

如果你尝试使用过STL,那么一定对迭代器不感到陌生,迭代器作为STL六大组件之一,是连接算法与容器之间的桥梁,其重要性不言而喻。

相信大家在刚接触STL的时候都会对这个问题感到困惑,那就是为什么STL中的算法可以适用于不同的容器?

这其实是源自于学习C语言和c++的类与对象部分这一时期的惯性思维,那就是函数的参数和对象的类型一定要匹配,比如作用于某个类的函数,我们就要根据这个类的行为来设计函数。这就导致了STL中的某些算法看起来是不合理的,比如为什么find可以作用于vector,也能作用于list,这两个容器连数据结构都不一样,为什么能有同样的效果?

这件事情就好比,将数学上将数字和几何学结合起来一样难以理解。但是数学上还真有一个东西将数字和几何学结合起来了,那就是函数图像。我们可以尝试理清一下函数图像是如何将集合和数字结合在一起的。一个数字,一个集合,明明是八竿子打不着的关系,就像list和vector一样。其实原理也很简单,其实只需要有一个东西作为这两者之间的桥梁一样,这便是坐标系。有了坐标系,我们就能通过函数来分析几何(圆的函数),也能通过几何来分析函数(微积分)。

那么迭代器和容器和算法的关系也是如此,有了迭代器,我们就能让算法使用在不同的容器上。那么迭代器又是怎么做到的呢?别急,我们慢慢看

容器与迭代器

相信大家用迭代器遍历整个容器应该是再熟悉不过的操作了吧。我们尝试分别用list、vector的迭代器遍历各自的容器。

	void TestIterator(){vector<int> v1{ 1,2,3,4,5,6,7,8,9,10 };list<int> list1{ 1,2,3,4,5,6,7,8,9,10 };vector<int>::iterator vi = v1.begin();//v1的迭代器list<int>::iterator li = list1.begin();//list1的迭代器while (vi != v1.end()){cout << *vi;//{1,2,3,4,5,6,7,8,9,10}vi++;//让迭代器访问下一个元素}while (li != list1.end()) {cout << *li;//{1,2,3,4,5,6,7,8,9,10}li++;//让迭代器访问下一个元素}}

我们观察上面的代码,可以发现一个有意思的事情,那就是无论是list还是vector的迭代器,其都支持++,和*的操作。实际上,STL中的迭代器都会支持以下操作

操作符执行操作
++让迭代器来到下一个元素
--让迭代器来到上一个元素
*访问迭代器中的元素
->访问迭代器中的元素的成员

这就发现一个很有意思的现象了,那就是无论STL中各个容器的数据结构和成员有多么的不一样,我们都能用相同的方法访问到容器中的元素,那就是通过迭代器。

算法与迭代器

我们以find()为例,在c++标准中,find()函数的声明如下:

template <class InputIterator, class T>InputIterator find (InputIterator first, InputIterator last,const T& val);

在调用find()函数时,我们需要传入同一容器中的两个迭代器,还有一个代表迭代器中的值。其中T是解引用迭代器得到的值的类型。其作用为:在[first,find)之间找到val这个元素,并返回val元素的迭代器,如果没有找到,就返回last

我们尝试使用一下这个函数:

	void TestFind(){vector<int> v1{ 1,2,3,4,5,6,7,8,9,10 };list<int> list1{ 1,2,3,4,5,6,7,8,9,10 };vector<int>::iterator viter;//v1的迭代器list<int>::iterator liter;//liter的迭代器viter=find(v1.begin(), v1.end(), 4);liter=find(list1.begin(), list1.end(), 11);if (viter == v1.end())cout << "not find" << endl;elsecout << "find " << *viter<<endl;if (liter == list1.end())cout << "not find" << endl;elsecout << "find" << *liter<<endl;}

运行结果如下:在v1容器中找到了4,并且返回了4这个位置的迭代器,在list1中并没有找到11,于是返回了list1.end(),说明没有找到这个元素。

如果我没有学习过STL,我会感到非常好奇的点是:list和vector在底层上是两个截然不同的数据结构,list的链表,而vector是顺序表,链表的遍历方式是通过节点遍历,而顺序表的遍历方式则是通过下标遍历。链表对于元素访问的方式是读取节点的值,而顺序表对于元素访问的方式则是去下标。但是在上述的两个例子当中,各自的迭代器竟然用相同的方式就完成了这些任务。

这里博主尝试模拟实现一个find(),希望方便读者进行理解:

template <class InputIterator, class T>
InputIterator myfind(InputIterator first,InputIterator last,const T& val)
{while (first != last){if (*first == val)return first;first++;}return last;
}

从这个模拟实现我们不难看出一个点,那就是STL中的算法不需要考虑容器的底层到底是什么数据结构,也不需要考虑这个数据结构遍历元素时需要用到什么算法,只需要用户能将容器的迭代器(无论是什么容器)传入函数中,通过对迭代器的一致操作(所有的迭代器都会的操作,比如*)进行操作即可。于是这个函数就变成了能用于各个容器的通用算法。

迭代器

我们讲了这么多有关于迭代器和容器,迭代器和算法之间的各个作用方式,但是想要了解迭代器,最好的方法还是从迭代器开始入手。博主在此之前曾写过list的模拟实现,但是这个博客有一点我很不满意,那就是关于list的迭代器被简单提到就一笔带过了,主要原因还是我想在list的模拟实现中展示更多的是容器的设计方式,而非迭代器。因此在完成那篇博客之后,我为list的迭代器写了这篇博客。

以list为例,我尝试为其设计一个迭代器。先来看看list中大概有什么内容。

template<class T>struct ListNode//结点{T _data;//数据ListNode* _next;//指向下一个节点的指针ListNode* _prev;//指向上一个节点的指针};template<class T>class list{typedef ListNode<T> Node;Node* _head;//指向链表头结点};

既然想要设计list的迭代器,就要思考一个点,那就是list的迭代器解引用后得到的东西是什么?是list本身,还是list的数据(listnode)?显然是后者,那么list的迭代器的底层显然是指向listnode的指针

template<class T, class ref, class ptr>struct listiterator{typedef ListNode<T> Node;typedef listiterator self;Node* linkptr;};

接着就是为其设计哪些迭代器的通用操作,比如解引用操作(*),成员访问(->)。自加和自减操作符,相等与不等操作符。

	template<class T, class ref, class ptr>struct listiterator{typedef ListNode<T> Node;typedef listiterator self;listiterator(Node* listnode = nullptr)//默认构造{linkptr = listnode;}self operator++()//前置++;self operator++(int)//后置++self operator--()//前置--self operator--(int)//后置++ref operator*();ptr operator->()bool operator!=(listiterator it)bool operator==(listiterator it);Node* linkptr;};

接着就是设计这些操作符的行为逻辑了。我们就拿最复杂的自加和自减操作符为例:
list的迭代器自加会来到下一个元素的位置,而list中下一个元素位置在哪呢?没错,就是下一个节点的位置。于是operator++应该是这样的:

self operator++()//前置++
{linkptr = linkptr->next;return linkptr;
}self operator++(int)//后置++
{listiterator tmp = linkptr;linkptr = linkptr->next;return tmp;
}

自减操作也是同理,让节点来到上一个节点的位置。

self operator--()
{linkptr = linkptr->prev;return linkptr;
}self operator--(int)
{listiterator tmp = linkptr;linkptr = linkptr->next;return tmp;
}

解引用和成员访问符的重载操作逻辑应该如下:解引用可以得到元素,也就是节点的值,而成员访问符由于还需要一个->才能访问元素的成员,于是只需要返回元素的指针即可

ref operator*()
{return linkptr->data;
}ptr operator->()
{return &(linkptr->data);
}

于是list的迭代器整体实现如下:

template<class T, class ref, class ptr>
struct listiterator
{typedef ListNode<T> Node;typedef listiterator self;listiterator(Node* listnode = nullptr){linkptr = listnode;}self operator++()//前置++{linkptr = linkptr->next;return linkptr;}self operator++(int){listiterator tmp = linkptr;linkptr = linkptr->next;return tmp;}self operator--(){linkptr = linkptr->prev;return linkptr;}self operator--(int){listiterator tmp = linkptr;linkptr = linkptr->next;return tmp;}ref operator*(){return linkptr->data;}ptr operator->(){return &(linkptr->data);}bool operator!=(listiterator it){return linkptr != it.linkptr;}bool operator==(listiterator it){return linkptr == it.linkptr;}Node* linkptr;
};

但是这还不够,因为我们可以发现迭代器是定义在类域中的,但是我们设计的迭代器似乎还在类外,而且还需要实例化才能用

没关系,只要简单的步骤就可以了

template<class T>class list{public:typedef ListNode<T> Node;typedef listiterator<T, T&, T*> iterator;typedef listiterator<T, const T&, const T*> const_iterator;Node* _head;//指向链表头结点};
}

在list类的内部为迭代器起一个别名即可,这样子list便有了可以用于算法的迭代器了。

总结

迭代器其实一个设计理念,不仅仅用用STL库,我们甚至还能在自定义类中用到迭代器,甚至还能将这些迭代器传到STL的算法中。

实际上迭代器是容器和算法之间的桥梁,如果没有迭代器,我们需要为每个容器设计底层不一样的算法,比如find算法,在list中就需要通过访问下一个节点来查找,而在vector中就需要通过访问下标来查找,非常麻烦。

而如果能设计出行为一致,底层不一致的迭代器,那么在设计算法时,就不需要考虑到容器之间的底层差异了。直接通过迭代器来操作,因为迭代器具有一致的行为(自加、自减等),就可以让一个算法服务于多个容器。

本博客的具体代码上传至博主的代码仓库,仓库链接放在主页。

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

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

相关文章

QT 创建文件 Ui 不允许使用不完整类型,可以尝试添加一下任何头文件

#include "debug.h" #include "qmessagebox.h" #pragma execution_character_set("utf-8") //QT 创建文件 Ui 不允许使用不完整类型,尝试添加一下任何头文件&#xff0c;或者添加ui_xx.h头文件 debug::debug(QWidget *parent) : QDialog(p…

Kotlin getter 和 setter

文章目录 定义field 字段&#xff08;Backing Fields&#xff09; 定义 我们可以为变量定义get&#xff08;无参数&#xff0c;有与变量同类型的放返回值&#xff09;或set&#xff08;有一个与变量类型相同的参数&#xff0c;返回Unit&#xff09;函数&#xff0c;在取值&…

【MATLAB源码-第224期】基于matlab的快跳频系统仿真采用4FSK,模拟了单音干扰,宽带干扰以及部分频带干扰,输出误码率曲线以及各节点图像。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 跳频通信系统概述 跳频通信系统是一种通过快速切换载波频率来进行信息传输的无线通信技术。它在军事和商业通信中广泛应用&#xff0c;具有较强的抗干扰和抗截获能力。系统设计主要包括信号调制、跳频序列生成、信道模拟以及…

dart 基本语法

//入口方法 main() 或 void main() //数据类型 原生数据类型 String int double bool null 注意&#xff1a;String 包函 ‘’ “” ‘’’ ‘’’ 三种形式复杂数据类型 list Set Map自定义数据类型 class inheritance动态数据类型 var 注&#xff1a;dart 是静态类型语言&a…

【Linux】Centos7升级内核的方法:yum更新(ELRepo)

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主。 &#x1f913; 同时欢迎大家关注其他专栏&#xff0c;我将分享Web前后端开发、人工智能、机器学习、深…

Proxyman 现代直观的 HTTP 调试代理应用程序

Proxyman 是一款现代而直观的 HTTP 调试代理应用程序&#xff0c;它的功能强大&#xff0c;使您可以轻松捕获、检查和操作 HTTP(s) 流量。不再让繁杂的网络调试工具阻碍您的工作&#xff0c;使用 Proxyman&#xff0c;您将轻松应对网络调试的挑战。 下载地址&#xff1a;https…

springboot undertow 文件上传文件过大异常

io.undertow.server.RequestTooBigException: UT000020 Connection terminated as request was larger than xxxx 修改yaml文件中关于undertow的配置项 server:undertow:# HTTP POST请求最大的大小# 默认0&#xff0c;无限制max-http-post-size: ${SERVER_UNDERTOW_MAX_HTTP_…

Elasticsearch 认证模拟题 - 10

一、题目 在索引 task8 中&#xff0c;写出满足以下条件的查询 title 中包含 my"或 me如果 tags 中包含 romatic movies&#xff0c;该条算分提高&#xff0c;如果不包含则算分不变。 PUT task8 {"mappings": {"properties": {"title":{…

Apple开发者macOS设备与描述文件Profile创建完整过程

安装并打开Apple Configurator 新建描述文件 输入macOS平台的描述文件的相关信息,然后选择证书 选择一个可用证书 存储描述文件 存储成功如下: 使用文本编辑器打开刚才保存的描述文件,找到设备名与UDID

Day12:rem 布局 和 less 使用

目标&#xff1a;使用 rem 和 less 完成移动端的布局。 一、移动 Web 基础 1、谷歌模拟器 在网页右键点“检查”或快捷键 F12&#xff0c;然后右边栏顶部第二个按钮切换设备为移动端&#xff0c;刷新网页&#xff0c;可以看到谷歌模拟器&#xff0c;可以切换模拟器型号、尺寸…

[已解决]ESP32-C3上传程序成功但没有反应的问题

ESP32-C3上传程序成功但没有反应的问题 ESP32-C3是一款功能强大的微控制器&#xff0c;常用于物联网&#xff08;IoT&#xff09;应用的开发和原型设计。然而&#xff0c;有时候在上传程序成功后&#xff0c;设备却没有任何反应&#xff0c;十分让人费解。通过各种尝试已解决这…

华为设备动态路由OSPF(单区域+多区域)实验

动态路由OSPF的配置 OSPF分类两种情况&#xff1a;单区域 多区域路由 OSPF单区域路由配置 OSPF&#xff1a;开放最短路径优先的路由协议。属于大型动态路由协议&#xff0c;适用于中大型的园区网。 网络拓扑&#xff1a; 配置步骤&#xff1a; 1.完成基本配置&#xff08;略&a…

《传感器系列》COD 传感器

环境监测小卫士&#xff1a;COD 传感器&#xff0c;能够精准检测化学需氧量。对于水质监测和环境保护有着至关重要的作用&#xff01; 优势解析&#xff1a; 一、实时监测与快速响应 COD传感器能够实现实时监测和快速响应&#xff0c;这是其最大的优势之一。传统的COD测定方法…

计算机网络-OSI七层参考模型与数据封装

目录 一、网络 1、网络的定义 2、网络的分类 3、网络的作用 4、网络的数据传输方式 5、网络的数据通讯方式 二、OSI七层参考模型 1、网络参考模型定义 2、分层的意义 3、分层与功能 4、TCP\IP五层模型 三、参考模型的协议 1、物理层 2、数据链路层 3、网络层 4…

ASP.NET Core的开发效率利器HotReload(带例子)

ASP.NET Core HotReload 示例 在 ASP.NET Core 中&#xff0c;HotReload 功能使开发者能够在不重新启动应用程序的情况下&#xff0c;动态地应用代码更改。这有助于提高开发效率&#xff0c;因为你可以立即看到代码更改的效果。以下是一个使用 HotReload 的示例&#xff1a; …

技术分享 | SpringBoot 流式输出时,正常输出后为何突然报错?

项目背景 一个 SpringBoot 项目同时使用了 Tomcat 的过滤器和 Spring 的拦截器&#xff0c;一些线程变量在过滤器中初始化并在拦截器中使用。该项目需要调用大语言模型进行流式输出。项目中&#xff0c;笔者使用 SpringBoot 的 ResponseEntity<StreamingResponseBody> 将…

R语言探索与分析14-美国房价及其影响因素分析

一、选题背景 以多元线性回归统计模型为基础&#xff0c;用R语言对美国部分地区房价数据进行建模预测&#xff0c;进而探究提高多元回 归线性模型精度的方法。先对数据进行探索性预处理&#xff0c;随后设置虚拟变量并建模得出预测结果&#xff0c;再使用方差膨胀因子对 多重共…

python panads读取保存数据

学习目标 能够使用Pandas读写文件中的数据 知道Pandas读取数据时常用参数index_col、parse_dates、sheet_name、index的作用和用法 知道Pandas和MySQL数据库进行读写交互的方法 1 读写文件 常用读写文件函数清单【查表】无需记忆 文件格式读取函数写入函数xlsxpd.read_exce…

SOLIDWORKS工艺软件 慧德敏学

SOLIDWORKS工艺软件功能介绍 1、快速制作工艺过程卡 a)调用模板&#xff0c;快速生成工艺过程卡中的工序、设备、简图以及工时等信息&#xff0c;支持“加工工艺卡”和“装配工艺卡”的制作 b)选择文件&#xff0c;快速读取现有工艺过程卡文件&#xff0c;提取数据并显示&…

【问题分析】WMS无焦点窗口的ANR问题 + transientLaunch介绍【Android 14】

问题描述 Monkey跑出的Camera发生ANR的问题&#xff0c;其实跟Camera无关&#xff0c;任意一个App都会在此场景下发生ANR&#xff0c;场景涉及到Launcher的RecentsActivity界面&#xff0c;和transientLaunch相关。 1 log分析 看问题发生的场景&#xff1a; 1、Camera App的…