C++:迭代器的封装思想
C++:迭代器的封装思想
- list迭代器实现
- 反向迭代器实现
本博客将通过实现list的迭代器,以及它的反向迭代器,来帮助大家理解迭代器的底层逻辑,以及封装思想。
list迭代器实现
迭代器是一个遍历容器的工具,其可以通过自增自减来改变位置,通过解引用来访问节点,也就是模仿指针的行为。这为算法库提供了统一的方式来访问容器,也降低了用户的学习成本,可以用相同的方式来访问不同的容器。
那么迭代器是如何做到模仿指针行为的?
这还要归功于运算符重载这一设计,在类中,我们可以通过运算符重载来决定一个类在面对不同运算符时的行为,那么我们是否可以将迭代器封装为一个类,然后利用运算符重载,使得迭代器可以使用++,–,*这样的操作符,从而模仿指针的行为呢?
STL的底层迭代器就是利用这样的方式实现的。
我们先来看到list的基本结构:
list节点:
template <class T>
struct list_node
{list_node<T>* _next;list_node<T>* _prev;T _data;
};
这是一个list的节点list_node,其有三个成员:_prev指向上一个节点,_next指向下一个节点,_data存储当前节点的数据。
template <class T>class list{typedef list_node<T> node;private:node* _head;};
这是一个list类,其将节点list_node<T>重命名为了node,方便后续使用。
唯一一个成员变量是哨兵位头节点指针_head。
接下来我们就讨论要如何设计这个list的迭代器:
毫无疑问,我们需要把这个迭代器封装为一个类,而这个类的内部我们需要什么,才能让迭代器访问不同的节点呢?
想要访问不同的节点,毫无疑问就需要不同节点的指针,所以我们的迭代器需要把一个指向节点的指针给封装起来,然后利用运算符重载,改变这个指针的行为,从而实现迭代器。
现在我们将这个迭代器命名为__list_iterator,先看看基础结构:
template <class T>
struct __list_iterator
{typedef list_node<T> node;node* _node;
};
为了方便我们使用节点的指针,我们重命名list_node<T>为node,而迭代器内部指向节点的指针名为_node。
构造函数:
代码如下:
__list_iterator(node* n): _node(n)
{}
这就是一个很简单的初始化过程,这里不过多赘述了,那么我们要如何使用这个迭代器呢?
看看list类中获取迭代器的函数:
typedef __list_iterator<T> iterator;iterator begin()
{return iterator(_head->_next);
}iterator end()
{return iterator(_head);
}
先将我们的迭代器重命名为iterator,方便后续使用。
可以看到,我们的begin()函数就是返回了哨兵位节点的后一个节点,也就是第一个节点的指针,但是普通的指针的++,–操作不符合我们的要求,于是我们利用iterator(_head->_next)这个方式,利用这个指针来构造一个匿名对象,也就是迭代器的匿名对象,此时我们就完成了指针的封装,得到的指针就是被我们修改了行为后的迭代器了。
end()函数同理,iterator(_head)构造一个迭代器,将指针进行封装,修改其行为。
接下来我们回到迭代器的实现:
operator++:
我们希望我们的迭代器可以在++的时候到达下一个节点的位置,也就是_node = _node->_next这样的行为。
所以我们的需求就明确了,当迭代器++的时候,实际发生的是_node = _node->_next而不是简单的指针偏移。
先看到我们的实现:
__list_iterator<T> & operator++()
{_node = _node->_next;return *this;
}
++是要进行返回的,将自增后的节点作为返回值,所以最后我们还要return *this;,将修改后的节点作为返回值。
后置++同理:
__list_iterator<T>& operator++(int)
{__list_iterator<T> tmp(*this);_node = _node->_next;return tmp;
}
由于后置++需要将自增前的值作为返回值,所以我们要先拷贝一份原先的值tmp,自增完成后,将原先的值tmp返回。
为了简化代码,我们可以将__list_iterator<T>重命名为self代表迭代器自己的类型。
最后代码如下:
typedef __list_iterator<T> self;self& operator++()
{_node = _node->_next;return *this;
}self& operator++(int)
{self tmp(*this);_node = _node->_next;return tmp;
}
这样一来,我们的迭代器++,表面上和指针自增一样,但是底层却是_node = _node->_next到达下一个节点的位置了。
operator–:
迭代器–同理,我们需要实现迭代器到达上一个节点,其实就是_node = _node->_prev。
实现如下:
self& operator--()
{_node = _node->_prev;return *this;
}self& operator--(int)
{self tmp(*this);_node = _node->_prev;return tmp;
}
迭代器判断相等:
想要判断两个迭代器是否相等,其实就是判断其内部封装的指针_node是否相等。
代码如下:
bool operator!=(const self& s)
{return _node != s._node;
}bool operator==(const self& s)
{return _node == s._node;
}
这个比较简单,不做赘述了。
operator*:
*是迭代器中十分重要的操作符,指针就是利用解引用来访问指向内容的,迭代器就模仿指针,也通过解引用来访问节点值。
迭代器内部的成员_node是指向节点的指针,那么访问这个节点的值就是:_node->_data了。
所以解引用代码如下:
T& operator*()
{return _node->_data;
}
但是这个代码有一个问题,那就是:如果我们的list被const修饰了怎么办?
这样的话,由于我们返回的是T&,而这个_data就有可能会被修改,这就不符合const的行为了。
是否我们要写一个重载,比如这样:
T& operator*()
{return _node->_data;
}const T& operator*()
{return _node->_data;
}
这是不行的,因为两个函数只有返回值不同,并不构成重载。
我们只希望返回值的类型不同,这要怎么办?
注意:我们的迭代器是利用模板生成的,而模板最大的用处就是泛型编程,可以无视类型的限制,我们完全可以给模板多传一个参数,让第二个参数作为operator*的返回值。
//-----迭代器-----
template <class T, class Ref>
struct __list_iterator
{typedef __list_iterator<T, Ref> self;Ref operator*(){return _node->_data;}
};
这样,当我们需要普通迭代器,第二个参数Ref就传入T&,如果需要const迭代器,那么第二个参数就传入const T&。
不过要注意的是,刚刚我们重命名了一个self, 即typedef __list_iterator<T> self,此时由于模板参数改变,这个重命名也要一起改变:typedef __list_iterator<T, Ref> self。
现在我们就可以在list中加入const迭代器了,接下来让类中定义的迭代器一起改变:
template <class T>
class list
{typedef __list_iterator<T, T&> iterator;typedef __list_iterator<T, const T&> const_iterator;//普通迭代器iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}//const迭代器const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}
};
看到两行typedef:
typedef __list_iterator<T, T&> iterator;
typedef __list_iterator<T, const T&> const_iterator;
当我们传入的第二个模板参数为T&,那么解引用的返回值就是可以修改的,此时就是一般的迭代器iterator。
当我们传入的第二个模板参数为const T&,那么解引用的返回值就是不可以修改的,此时就是const迭代器const_iterator。
相比于开始,我们现在多了一个迭代器const_iterator;,我们的this指针被const修饰时,说明此时需要调用const迭代器,那么我们就返回const_iterator类型的迭代器。
const_iterator begin() const
{return const_iterator(_head->_next);
}const_iterator end() const
{return const_iterator(_head);
}
operator->:
接下面我们看到最后一个问题,那就是类的嵌套问题,假设我们的list内部是一个A类:
class A
{
public:int num;
};
那么如果我们想要通过迭代器访问A类的num成员,要怎么做?
假设我们有一个list的迭代器it,接下来我用it尝试访问这个A的成员num。
(*it).num
先解引用迭代器(*it),此时就得到了list内部的A类,然后再利用.来访问内部的num成员。
这样访问是没有问题的,但是我们的迭代器是模仿指针的行为,我们会对一个指针先解引用,再用点操作符访问成员吗?
我们完全可以指针 -> 成员这样访问。
也就是说,我们还要对迭代器重载一个->操作符,实现这种直接访问的方式
代码:
T* operator->()
{return &_node->_data;
}
我们的返回值是_data的地址,而_data就是A类,所以我们可以就得到了一个A类的指针,此时T*就是其实就是A*。
那么我们看看现在要如何访问:
list.operator->()->num
你也许不是很能理解以上代码,我们拆解一下:
list.operator->()返回了一个A*类型的指针,而A*类型的指针可以直接访问num:
A* ptr;
ptr->num
所以以上代码list.operator->()->num就可以访问到num这个成员了。
而.operator->()其实就是->操作符,所以可以变形为以下代码:
list->->num
连用->->不太美观,也容易让人费解,但是编译器会将两个->优化为一个->,所以此时我们已经可以直接像指针一样访问成员了:
list->num
我们再回顾一遍推导过程:
list.operator->()->num==list->->num==list->num
这个访问和刚刚的operator*会遇到同样的问题,那就是const问题,所以我们要传入第三个模板参数Ptr,来控制operator->的返回值:
template <class T, class Ref, class Ptr>
struct __list_iterator
{Ptr operator->(){return &_node->_data;}
};
在list中:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
至此,我们就实现了一个list的迭代器了。
代码展示:
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* n): _node(n){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}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;}bool operator!=(const self& s){return _node != s._node;}bool operator==(const self& s){return _node == s._node;}
};
反向迭代器实现
那么我们要如何实现反向迭代器呢?
一开始我们实现正向迭代器的时候,遇到的问题就是:原生指针的行为不符合我们的要求,于是我们将原生指针进行了封装,利用运算符重载来修改其行为。
那么我们的反向迭代器还需要封装一个原生指针吗?
其实我们可以换一个思路:现在我们有正向迭代器,而正向迭代器并不符合反向迭代器的预期行为,我们不妨对正向迭代器进行一次封装,让其++变成–,–变成++,这不就是一个反向迭代器了吗?
结构如下:
template <class Iterator, class Ref, class Ptr>
struct ReverseIterator
{Iterator _cur;
};
我们定义了一个反向迭代器的类ReverseIterator,在内部封装了一个迭代器Iterator _cur,接下来我们就重载这个反向迭代器,让其行为符合预期:
先看看我们在list中是如何定义反向迭代器的:
template <class T>class list{typedef ReverseIterator<iterator, T&, T*> reverse_iterator;//反向迭代器reverse_iterator rbegin(){return reverse_iterator(end());}reverse_iterator rend(){return reverse_iterator(begin());}};
首先我们重命名了反向迭代器typedef ReverseIterator<iterator, T&, T*> reverse_iterator;,其第一个模板参数为iterator,也就是我们封装的正向迭代器。
随后在rbegin()中,我们直接将end()返回的末尾迭代器,作为我们反向迭代器的起始迭代器rbegin();
将begin()返回的正向迭代器的起始迭代器,作为我们反向迭代器的末尾迭代器。
注意,end()返回的是最后一个元素的后面一个位置,所以我们在后续++,–操作时,要进行其他的操作,来矫正这个位置的偏差。
operator++:
现在由于我们是反向迭代器,我们的反向迭代器++,其实就是正向迭代器的–。
代码如下:
self& operator++()
{--_cur;return *this;
}
operator*
由于我们的迭代器位置是往后偏一位的,所以我们要返回前一个位置的迭代器而不是当前位置的迭代器:
Ref operator*()
{Iterator tmp = _cur;--tmp;return *tmp;
}
我们用tmp拷贝了一份当前的迭代器,随后--tmp,将其往前走一个位置,再返回tmp位置的迭代器,这样就可以矫正我们起初的位置偏差了。
而其它的操作符重载,几乎没有太大的差别,基本就围绕以上两种类型的改动,此处我们直接展示代码了:
template <class Iterator, class Ref, class Ptr>struct ReverseIterator{typedef ReverseIterator<Iterator, Ref, Ptr> self;Iterator _cur;ReverseIterator(Iterator it):_cur(it){}Ref operator*(){Iterator tmp = _cur;--tmp;return *tmp;}Ptr operator->(){Iterator tmp = _cur;--tmp;return &tmp->_data;}self& operator++(){--_cur;return *this;}self& operator++(int){self tmp(*this);--_cur;return tmp;}self& operator--(){++_cur;return *this;}self& operator--(int){self tmp(*this);++_cur;return tmp;}bool operator!=(const self& s){return _cur != s._cur;}bool operator==(const self& s){return _cur == s._cur;}};
这个反向迭代器的实现,可以作用于任何正向迭代器,也就是说,不论是list,vector等等各种容器,都可以使用这一套反向迭代器来封装原本的迭代器,从而获得反向迭代器,
相关文章:
C++:迭代器的封装思想
C:迭代器的封装思想 list迭代器实现反向迭代器实现 本博客将通过实现list的迭代器,以及它的反向迭代器,来帮助大家理解迭代器的底层逻辑,以及封装思想。 list迭代器实现 迭代器是一个遍历容器的工具,其可以通过自增自…...
飞天使-k8s知识点17-kubernetes实操2-pod探针的使用
文章目录 探针的使用容器探针启动实验1-启动探针的使用-startupprobeLiveness Probes 和 Readiness Probes演示若存在started.html 则进行 探针的使用 kubectl edit deploy -n kube-system corednslivenessprobe 的使用 livenessProbe:failureThreshold: 5httpGet:path: /heal…...
tee漏洞学习-翻译-3:TrustZone exploit for MSM8974
原文:http://bits-please.blogspot.com/2015/08/full-trustzone-exploit-for-msm8974.html 在这篇博文中,我们将介绍利用上一篇文章中描述的 TrustZone 漏洞的完整过程。 在开发此漏洞时,我只使用了我值得信赖的(个人࿰…...
rust递归遍历磁盘目录及文件
Std库实现 //遍历dir目录,找出修改日期距离当前超过age天的文件名称,存入file_list中 fn visit_dir(dir: &Path, file_list: &mut Vec<String>, age: u64) -> io::Result<()> {if dir.is_dir() {for entry in fs::read_dir(dir)…...
C语言每日一题(56)平衡二叉树
力扣网 110 平衡二叉树 题目描述 给定一个二叉树,判断它是否是高度平衡的二叉树。 本题中,一棵高度平衡二叉树定义为: 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。 示例 1: 输入:root [3,9,20,…...
Flutter Android开发 梳理Google Material Design颜色体系
前言 做安卓开发(Kotlin语言),Flutter开发的人员应该都听说过谷歌一直推崇的Material Design,而Material Design Color是其推崇的颜色体系,具体来说,Material Design Color是一套旨在帮助设计师和开发者创…...
每日五道java面试题之java基础篇(六)
目录: 第一题:Java 创建对象有哪⼏种⽅式?第二题 .Integer a 127,Integer b 127;Integer c 128,Integer d 128;相等吗?第三题.Object 类的常⻅⽅法?第四题 List和Set的区别第五题 ArrayList和…...
c++ STL系列——(五)map
目录 引言 特点 包含头文件 基本特性 基本操作 插入元素 访问元素 移除元素 检查是否包含某个键 获取元素数量 高级特性 迭代器 自定义比较函数 实际应用 统计字符出现次数 缓存最近访问的元素 总结 引言 在C中,标准模板库(STL…...
Huggingface 文档翻译完毕
Accelerate 0.27 中文文档音频课程文档AutoTrain 中文文档AWS 中文文档竞赛中文文档Diffusers 0.26 中文文档深度强化学习课程文档数据集服务器中文文档Datasets 2.17 中文文档 Evaluate 0.4 中文文档Huggingface.js 中文文档Hub 中文文档Hub 客户端库 JS 0.20 中文文档推理 AP…...
C++中类的6个默认成员函数 【拷贝构造函数】
文章目录 拷贝构造函数的使用拷贝构造对于自定义类型【浅拷贝】深拷贝拷贝构造函数典型调用场景 拷贝构造函数的使用 在前几章学习对象的时候,我们有的时候需要一个与已存在对象一某一样的新对象 那在创建对象时,可否创建一个与已存在对象一某一样的新对…...
【前端高频面试题--Vuex下篇】
🚀 作者 :“码上有前” 🚀 文章简介 :前端高频面试题 🚀 欢迎小伙伴们 点赞👍、收藏⭐、留言💬前端高频面试题--Vuex篇 往期精彩内容Vuex 的原理Vuex中action和mutation的区别Vuex 和 localStor…...
MySQL性能调优篇(4)-查询语句的优化与重构
MySQL数据库查询语句的优化与重构 MySQL是一种常用的关系型数据库管理系统,广泛应用于Web开发中。在实际应用中,对数据库查询语句的优化和重构是提高应用性能和响应速度的重要手段。本文将介绍一些常见的优化技巧和重构方法,帮助开发者提高数…...
LInux、源码编译安装
步骤: 步骤1:安装开发工具gcc与make,释放源代码至指定目录 yum -y install gcc make 步骤2:tar解包,释放源代码至指定目录 tar -xf /root/tools.tar.gz -C /usr/local 步骤3:./configure 配置,…...
wordpress好的网站主题
有什么好的网站主题,都分享在这里了。 蓝色风格的wordpress模板,好的wordpress网站主题,需要既好看,又好用。 https://www.zhanyes.com/qiye/6305.html 血红色的好看的wordpress主题,布局经典,设计好的&am…...
【Java多线程】对进程与线程的理解
目录 1、进程/任务(Process/Task) 2、进程控制块抽象(PCB Process Control Block) 2.1、PCB重要属性 2.2、PCB中支持进程调度的一些属性 3、 内存分配 —— 内存管理(Memory Manage) 4、线程(Thread)…...
C# CAD交互界面-自定义面板集-查找定位(六)
运行环境 vs2022 c# cad2016 调试成功 一、代码说明 1. 类成员变量声明: List<ObjectId> objectIds new List<ObjectId>(); // 用于存储AutoCAD实体对象的ObjectId列表 private static Autodesk.AutoCAD.Windows.PaletteSet _ps2; // 自定义浮动面板…...
5.7 BCC工具之disksnoop.py解读
一,disksnoop.py简介 disksnoop工具用于追踪块设备的I/O操作的延迟,它会在每次I/O执行完成后打印一行摘要信息。我们根据这些摘要日志,来分析当前的I/O操作是否存在延迟,以判断I/O是否达到了瓶颈。 二,代码示例 #!/usr/bin/python # # disksnoop.py Trace block device…...
QT:实现图片选择器
一、效果图 二、用到的类 qApp:可以快速获取到项目目录位置。 QSettings :编写config文件,记录上次打开图片的位置,下次打开图片会从上次的位置查找图片。 QPixmap:用于图片的缩放,防止图片过小࿰…...
LLM大模型相关问题汇总---包括问题与答案
一、基础篇 1. 目前主流的开源模型体系有哪些? - Transformer体系:由Google提出的Transformer模型及其变体,如BERT、GPT等。 - PyTorch Lightning:一个基于PyTorch的轻量级深度学习框架,用于快速原型设计和实验…...
自动化测试定位不到元素怎么办?
1.动态id定位不到元素 分析原因:每次打开页面,ID都会变化。用ID去找元素,每次刷新页面ID都会发生变化。 解决方案:推荐使用xpath的相对路径方法或者cssSelector查找到该元素。 2.iframe原因定位不到元素 分析原因:…...
【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...
【Linux】Linux 系统默认的目录及作用说明
博主介绍:✌全网粉丝23W,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...
wpf在image控件上快速显示内存图像
wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…...
水泥厂自动化升级利器:Devicenet转Modbus rtu协议转换网关
在水泥厂的生产流程中,工业自动化网关起着至关重要的作用,尤其是JH-DVN-RTU疆鸿智能Devicenet转Modbus rtu协议转换网关,为水泥厂实现高效生产与精准控制提供了有力支持。 水泥厂设备众多,其中不少设备采用Devicenet协议。Devicen…...
车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...
聚六亚甲基单胍盐酸盐市场深度解析:现状、挑战与机遇
根据 QYResearch 发布的市场报告显示,全球市场规模预计在 2031 年达到 9848 万美元,2025 - 2031 年期间年复合增长率(CAGR)为 3.7%。在竞争格局上,市场集中度较高,2024 年全球前十强厂商占据约 74.0% 的市场…...
高分辨率图像合成归一化流扩展
大家读完觉得有帮助记得关注和点赞!!! 1 摘要 我们提出了STARFlow,一种基于归一化流的可扩展生成模型,它在高分辨率图像合成方面取得了强大的性能。STARFlow的主要构建块是Transformer自回归流(TARFlow&am…...
【java面试】微服务篇
【java面试】微服务篇 一、总体框架二、Springcloud(一)Springcloud五大组件(二)服务注册和发现1、Eureka2、Nacos (三)负载均衡1、Ribbon负载均衡流程2、Ribbon负载均衡策略3、自定义负载均衡策略4、总结 …...
C#最佳实践:为何优先使用as或is而非强制转换
C#最佳实践:为何优先使用as或is而非强制转换 在 C# 的编程世界里,类型转换是我们经常会遇到的操作。就像在现实生活中,我们可能需要把不同形状的物品重新整理归类一样,在代码里,我们也常常需要将一个数据类型转换为另…...
Linux中INADDR_ANY详解
在Linux网络编程中,INADDR_ANY 是一个特殊的IPv4地址常量(定义在 <netinet/in.h> 头文件中),用于表示绑定到所有可用网络接口的地址。它是服务器程序中的常见用法,允许套接字监听所有本地IP地址上的连接请求。 关…...
