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;
}

但是这个代码有一个问题,那就是:如果我们的listconst修饰了怎么办?
这样的话,由于我们返回的是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;}};

这个反向迭代器的实现,可以作用于任何正向迭代器,也就是说,不论是listvector等等各种容器,都可以使用这一套反向迭代器来封装原本的迭代器,从而获得反向迭代器,


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

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

相关文章

Linux POSIX信号量 线程池

Linux POSIX信号量 线程池 一. 什么是POSIX信号量&#xff1f;二. POSIX信号量实现原理三. POSIX信号量接口函数四. 基于环形队列的生产消费模型五. 线程池 一. 什么是POSIX信号量&#xff1f; POSIX信号量是一种用于同步和互斥操作的机制&#xff0c;属于POSIX&#xff08;Po…

项目管理工具软件Maven趣闻

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl Maven这个单词来自于意第绪语&#xff08;Yiddish&#xff09;&#xff0c;这是一种与德语和希伯来语有密切关系的犹太民族语言。在这个语境中&#xff0c;Maven意为“知识的…

ChatGPT高效提问—prompt实践(智能辅导-心理咨询-职业规划)

ChatGPT高效提问—prompt实践&#xff08;智能辅导-心理咨询-职业规划&#xff09; ​ 智能辅导是指利用人工智能技术&#xff0c;为学习者提供个性化、高效的学习辅助服务。它基于大数据分析和机器学习算法&#xff0c;可以针对学习者的学习行为、状态和能力进行评估和预测&a…

Shell - 学习笔记 - 2.15 - Shell关联数组(下标是字符串的数组)

现在最新的 Bash Shell 已经支持关联数组了。关联数组使用字符串作为下标&#xff0c;而不是整数&#xff0c;这样可以做到见名知意。 关联数组也称为“键值对&#xff08;key-value&#xff09;”数组&#xff0c;键&#xff08;key&#xff09;也即字符串形式的数组下标&…

AutoSAR(基础入门篇)10.1-Autosar_Ecum模式管理概述

目录 一、什么是模式管理 二、再谈BswM 1、BswM概述 2、BswM执行流程 三、再谈EcuM 模式管理应该算是我们实践篇中较难的内容了,还有就是诊断那章也比较难。因为模式管理里面可能回涉及到很多的名词,很多的特性,所以博主准 备分个8次左右来讲解这些内容。但是在实际的应…

如何在 Angular 中使用环境变量

简介 如果你正在构建一个使用 API 的应用程序&#xff0c;你会想在开发过程中使用测试环境的 API 密钥&#xff0c;而在生产环境中使用生产环境的 API 密钥。在 Angular 中&#xff0c;你可以通过 environment.ts 文件创建环境变量。 在本教程中&#xff0c;你将学习如何在 A…

【Java万花筒】数据流的舵手:大数据处理和调度库对比指南

智慧的导航仪&#xff1a;为您的数据流选择正确的大数据处理和调度库 前言 在如今的信息时代&#xff0c;大数据处理和调度已经成为许多企业和组织中关键的任务。为了有效地处理和管理大规模数据流&#xff0c;选择适合的调度库是至关重要的。本文将介绍几种常用的大数据处理…

【前端工程化面试题】使用 webpack 来优化前端性能/ webpack的功能

这个题目实际上就是来回答 webpack 是干啥的&#xff0c;你对webpack的理解&#xff0c;都是一个问题。 &#xff08;1&#xff09;对 webpack 的理解 webpack 为啥提出 webpack 是啥 webpack 的主要功能 前端开发通常是基于模块化的&#xff0c;为了提高开发效率&#xff0…

MATLAB知识点:datasample函数(★★☆☆☆)随机抽样的函数,能对矩阵数据进行随机抽样

讲解视频&#xff1a;可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇&#xff08;数学建模清风主讲&#xff0c;适合零基础同学观看&#xff09;_哔哩哔哩_bilibili 节选自第3章&#xff1a;课后习题讲解中拓展的函数 在讲解第三…

数据类型与变量

目录 作业回顾 有关JDK, JRE, JVM三者&#xff1a; 判断题 新课学习 字面常量 数据类型 变量 整型变量 长整型变量 短整型变量 字节型变量 浮点型变量 字符型变量 布尔型变量 类型转换 自动类型转换&#xff08;隐式&#xff09; 强制类型转换&#xff08;显式…

Navicat安装使用连接MySQL

目录 安装登录MySQL登录MySQL用Navicat连接MySQL 安装 选择“我同意”&#xff0c;点击下一步。 选择安装的目标文件夹&#xff0c;点击下一步。 点击下一步。 点击下一步。 点击安装。 软件安装需要一些时间&#xff0c;请耐心等待 点击“完成”。 注册 输入 密钥&#x…

Crypto-RSA3

题目&#xff1a;&#xff08;BUUCTF在线评测 (buuoj.cn)&#xff09; 共模攻击 ​ 前提&#xff1a;有两组及以上的RSA加密过程&#xff0c;而且其中两次的m和n都是相同的&#xff0c;那么就可以在不计算出d而直接计算出m的值。 ​ 设模数为n&#xff0c;两个用户的公钥分别为…

全栈笔记_浏览器扩展篇(manifest.json文件介绍)

manifest.json介绍 是web扩展技术必不可少的插件配置文件,放在根目录作用: 指定插件的基本信息 name:名称manifest_version:manifest.json文件的版本号,可以写2或3version:版本description:描述定义插件的行为: browser_action:添加一个操作按钮到浏览器工具栏,点击按…

LeetCode 0103.二叉树的锯齿形层序遍历:层序遍历 + 适时翻转

【LetMeFly】103.二叉树的锯齿形层序遍历&#xff1a;层序遍历 适时翻转 力扣题目链接&#xff1a;https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/ 给你二叉树的根节点 root &#xff0c;返回其节点值的 锯齿形层序遍历 。&#xff08;即先从左往…

关于C++中的深拷贝

说到深拷贝&#xff0c;是相对于浅拷贝而言的。弄清了浅拷贝&#xff0c;深拷贝也就不言自明了。对C初学者而言&#xff0c;所谓浅拷贝在编写程序过程中往往是无感的。我们一般在写一个类时&#xff0c;多数情况我们只是写了成员变量、成员函数&#xff0c;有时为了赋初值方便&…

Java与JavaScript同源不同性

Java是目前编程领域使用非常广泛的编程语言&#xff0c;相较于JavaScript&#xff0c;Java更被人们熟知。很多Java程序员想学门脚本语言&#xff0c;一看JavaScript和Java这么像&#xff0c;很有亲切感&#xff0c;那干脆就学它了&#xff0c;这也间接的帮助了JavaScript的发展…

HTML | DOM | 网页前端 | 常见HTML标签总结

文章目录 1.前端开发简单分类2.前端开发环境配置3.HTML的简单介绍4.常用的HTML标签介绍 1.前端开发简单分类 前端开发&#xff0c;这里是一个广义的概念&#xff0c;不单指网页开发&#xff0c;它的常见分类 网页开发&#xff1a;前端开发的主要领域&#xff0c;使用HTML、CS…

OpenCV中的边缘检测技术及实现

介绍: 边缘检测是计算机视觉中非常重要的技术之一。它用于有效地识别图像中的边缘和轮廓&#xff0c;对于图像分析和目标检测任务至关重要。OpenCV提供了多种边缘检测技术的实现&#xff0c;本博客将介绍其中的两种常用方法&#xff1a;Canny边缘检测和Sobel边缘检测。 理论介…

【C语言】(25)文件包含include

#include是C语言中的预处理指令之一&#xff0c;用于在当前文件中包含另一个文件的内容。用于模块化和代码重用的基本机制。合理使用#include可以使代码结构更加清晰&#xff0c;易于管理和维护。 #include主要用于包含标准库头文件或自定义头文件。 两种形式的#include #in…

C语言程序设计(第四版)—习题7程序设计题

目录 1.选择法排序。 2.求一批整数中出现最多的数字。 3.判断上三角矩阵。 4.求矩阵各行元素之和。 5.求鞍点。 6.统计大写辅音字母。 7.字符串替换。 8.字符串转换成十进制整数。 1.选择法排序。 输入一个正整数n&#xff08;1&#xff1c;n≤10&#xff09;&#xf…