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原因定位不到元素 分析原因:…...
【R语言编程——数据调用】
这里写自定义目录标题 可用库及数据集外部数据导入方法查看数据集信息 在R语言中,有多个库支持调用内置数据集或外部数据,包括studentdata等教学或示例数据集。以下是常见的库和方法: 可用库及数据集 openintro库 该库包含多个教学数据集&a…...
【向量库】Weaviate 搜索与索引技术:从基础概念到性能优化
文章目录 零、概述一、搜索技术分类1. 向量搜索:捕捉语义的智能检索2. 关键字搜索:精确匹配的传统方案3. 混合搜索:语义与精确的双重保障 二、向量检索技术分类1. HNSW索引:大规模数据的高效引擎2. Flat索引:小规模数据…...

C#学习12——预处理
一、预处理指令: 解释:是在编译前由预处理器执行的命令,用于控制编译过程。这些命令以 # 开头,每行只能有一个预处理指令,且不能包含在方法或类中。 个人理解:就是游戏里面的备战阶段(不同对局…...
js 比较两个对象的值,不相等就push对象的key
在JavaScript中,比较两个对象(object)的值并找出不相等的key,可以通过多种方法实现。下面是一些常用的方法: 方法1:使用JSON.stringify 这种方法适用于简单的对象,其中对象的值是基本类型或可…...

下一代设备健康管理解决方案:基于多源异构数据融合的智能运维架构
导语: 在工业4.0深度演进的关键节点,传统设备管理面临数据孤岛、误诊率高、运维滞后三大致命瓶颈。本文解析基于边缘智能与数字孪生的新一代解决方案架构,并实测验证中讯烛龙PHM-X系统如何通过多模态感知→智能诊断→自主决策闭环,…...

MQTT协议:物联网时代的通信基石
MQTT协议:物联网时代的通信基石 在当今快速发展的物联网(IoT)时代,设备之间的通信变得尤为重要。MQTT(Message Queuing Telemetry Transport)协议作为一种轻量级的消息传输协议,正逐渐成为物联…...

基于开源AI大模型AI智能名片S2B2C商城小程序源码的中等平台型社交电商运营模式研究
摘要:本文聚焦中等平台型社交电商,探讨其与传统微商及大型社交电商平台的差异,尤其关注产品品类管理对代理运营的影响。通过引入开源AI大模型、AI智能名片与S2B2C商城小程序源码技术,构建智能化运营体系。研究结果表明,…...
Java编程中常见的条件链与继承陷阱
格式错误的if-else条件链 典型结构与常见错误模式 在Java编程中,if-else条件链是一种常见的多条件处理模式,其标准结构如下: if (condition1) {// 处理逻辑1 } else if (condition2) {// 处理逻辑2 } else...
JSON解析崩溃原因及解决方案
问题记录: /************************************************| * 描述: 将ID124执行NFC操作-JSON解析为结构体* 函数名: cJSON_ID124_to_struct* 参数[ I]: *json_string 待解析的指针* 参数[II]: *wireless_rxd 结构体指针* 返回: 成功返回0 失…...

Python数据可视化科技图表绘制系列教程(二)
目录 表格风格图 使用Seaborn函数绘图 设置图表风格 设置颜色主题 图表分面 绘图过程 使用绘图函数绘图 定义主题 分面1 分面2 【声明】:未经版权人书面许可,任何单位或个人不得以任何形式复制、发行、出租、改编、汇编、传播、展示或利用本博…...