C++ 迭代器与反向迭代器

目录

一,什么是迭代器

1,定义

2,迭代器的设计思维

3,迭代器种类

二,迭代器与容器

1,容器中的迭代器

2,迭代器失效问题

三,迭代器的类型萃取(traits)

四,反向迭代器

1,什么是适配器(adapters)

2,什么是反向迭代器

3,反向迭代器的实现


一,什么是迭代器

1,定义

迭代器(iterators)是一种抽象的设计概念,是一种设计模式,它的定义如下:提供一种方法,使之能够按照顺序依次访问某个容器中的各个元素,而又无需暴露该容器的内部表述方式。

2,迭代器的设计思维

不论是泛型思维或 STL 的实际运用,迭代器(iterators)都扮演着重要的角色。STL 的中心思想在于:将数据容器(containers)和算法(algorithms)分开,彼此独立设计,最后再以一剂粘合剂将它们撮合在一起,而迭代器就是这个粘合剂

迭代器是一种行为类似指针的对象,而指针的各种行为中最常见也最重要的便是解引用(dereference)和成员访问(member access),因此,迭代器最重要的编程工作就是对 operator* 和 operator-> 进行重载(overloading)工作。当我们对一个迭代器对象解引用时,我们就会得到这个迭代器所指向的内容:

vector<int> vec = {1,2,3,4};
auto iter = vec.begin();    // vec.begin() 会返回该容器中的第一个元素的迭代器
*iter;                      // 我们对这个迭代器解引用就会得到 vec 中的第一个元素                    

任何一个 STL 算法,都需要获得由一对迭代器所标示的区间用以表示操作范围。这一对迭代器所标示的是个所谓的左闭右开区间,以 [first, last) 表示。也就是说,整个实际范围从 first 开始,直到 last-1。迭代器 last 所指的是最后一个元素的下一位置。这种左闭右开的设计会带来许多的便利,例如下面这个 STL 算法的循环设计:

// find 接收两个迭代器与一个目标,在这两个迭代器构成的范围内搜寻此目标
template<class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& value) {
	while (first != last and *first != value)
		++first;
	return first;
}
// 如果返回的迭代器是 last, 则表示没有找到目标

左闭右开示意图:

下面来演示一下容器、算法、迭代器是怎么合作的,以 find 函数为例:


// 只要我们传入不同的迭代器,find 函数就能够对不同的容器进行查找操作
vector<char> vec = { 'a','b','c','d','e'};
list<int> lt = { 1,2,3,4,5,6 };
	
auto it1 = find(vec.begin(), vec.end(), 'd');
if (it1 == vec.end()) cout << "not found" << endl;
else cout << "found" << endl;

auto it2 = find(lt.begin(), lt.end(), 5);
// ...

3,迭代器种类

在 C++ 中,常见的迭代器种类包括以下几种:

① 前向迭代器(Forward Iterator):它们适用于单向遍历容器的所有元素,支持递增操作(++)。例如:单向链表(std::forward list)使用的就是这种迭代器。

② 双向迭代器(Bidirectional Iterator):它们是在前向迭代器的基础上增加了递减操作(--)的迭代器,可以向前或向后遍历容器中的元素,适用于双向遍历容器的所有元素。例如:双向链表(std::list)使用的就是双向迭代器

③  随机访问迭代器(Random Access Iterator):它们是功能最为强大的迭代器,支持任意两个迭代器之间的定位、偏移、比较等操作,可以以常量时间实现跳跃访问。除了支持双向迭代器的所有操作外,还支持迭代器的加减法运算以及比较大小的运算。例如:对 std::vector 容器进行随机访问时使用的就是随机访问迭代器。

二,迭代器与容器

1,容器中的迭代器

在 STL 中几乎每个容器都会支持相应的迭代器类型。我们暂且以 std::list 为例,来看看一个容器要设计出迭代器,需要做哪些事情。前面有提到迭代器的行为类似于指针,所以在迭代器类的模板参数列表中一定要有这个“指针”所指向的类型:

template<class T>    // T 是需要在链表中存放的数据类型
class List_Iterator{

    typedef List_Iterator<T> self;
    
    // 因为 std::list 是双向链表, 那自然我们要设计的迭代器肯定是双向迭代器了
    // 下面是双向迭代器需要实现的基本功能

    // 解引用与访问
    T& operator*();
    T* operator->();
    
    // 前置 ++,-- 与后置 ++,--
    self operator++();
    self operator++(int);
    self operator--();
    self operator--(int);
    
    // 迭代器之间的比较
    bool operator==();
    bool operator!=();

};

在容器 list 中,我们就可以这样来使用这个迭代器:

// 链表类
template<class T>
class list{
public:
    typedef List_Iterator<T> iterator;
    // ...
};

不过,我们还要考虑一下 const 迭代器

typedef List_Iterator<const T> const_iterator;    // 这样行吗?

迭代器最重要的编程工作就是对 operator* 和 operator-> 进行重载工作,所以迭代器中肯定会有返回值为 T& 以及 T* 的函数。因此我们要考虑,按照上面的 const 迭代器的设计,T& 和 T* 符合我们的预期吗?我们来看以下代码:

template<class T>
class List_Iterator {
private:
    T data = 0;
public:
	List_Iterator() {}
	T& reference() { return data; }
	T* pointer() { return &data; }
};

int main() {
	List_Iterator<const int> cit;

	// 我们所期望的
	const int& expect_ref = 0;
	const int* expect_ptr = nullptr;

	// 实际的
	auto& real_ref = cit.reference();
	auto real_ptr = cit.pointer();

	return 0;
}

我们调出监视窗口:

 

嗯,这样确实符合我们的预期。

 但是我们还要考虑到,如果迭代器需要使用原来的数据类型呢?例如:

template<class T>
class List_Iterator {
public:
    // List_Node 是链表的节点类型, 如果我们需要 T 来去拿到相应的节点类型呢?
    typedef List_Node<T> Node;
};

List_Iterator<const int> iter;
// 如果我们传入的模板参数是 const int, 那我们在 List_Iterator 中所取得的 Node
// 就是 List_Node<const int>, 而我们所期望的 Node 是 List_Node<int>

所以很明显,typedef List_Iterator<const T> const_iterator; 按照这样来设计肯定不行。

因此,为了得到我们想要的 const 迭代器,我们需要在迭代器类的模板参数列表中新添加两个参数:引用类型与指针类型。解决方案如下:

// T 是需要在链表中存放的数据类型, Ref 是引用类型, Ptr 是指针类型
template<class T, class Ref, class Ptr>    
class List_Iterator{

    typedef List_Iterator<T, Ref, Ptr> self;
    
    // 因为在某些场景下需要用普通迭代器来构造 const 迭代器,所以我们需要下面这两句
    typedef List_Iterator<T, T&, T*> iterator;
    List_Iterator(iterator iter);

    // 为了提高代码的可读性和可维护性, 使得容器的模板参数更具有可定制性和通用性,
    // 这里添加几个 typedef 来定义一下相应的类型, 这个非常重要!!!
    typedef T	value_type;
	typedef Ref reference;
	typedef Ptr pointer;
    
    // 解引用与访问
    reference operator*();
    pointer operator->();
    
    // 前置 ++,-- 与后置 ++,--
    self operator++();
    self operator++(int);
    self operator--();
    self operator--(int);
    
    // 迭代器之间的比较
    bool operator==();
    bool operator!=();

};

// 链表类
template<class T>
class list{
public:
    typedef List_Iterator<T, T&, T*> iterator;
    typedef List_Iterator<T, const T&, const T*> const_iterator;
    // ...
};

现在,我们已经设计出来了 list 迭代器类型的框架了。不过要想实现一个针对 list 而设计的迭代器,那必须得先对 list 的实现细节有着非常充分的了解(尤其是实现 ++、-- 操作时),虽然容器迭代器的框架都是类似的,但是迭代器的具体实现工作还是就交给容器的设计者好啦。

对 list 迭代器的实现细节感兴趣的话可以看看这个:C++ 简单模拟实现 STL 中的 list 与 queue-CSDN博客

2,迭代器失效问题

会引起其底层空间改变的操作,都有可能使迭代器失效,我们来看下面这个例子:

list<int> lt = {1,2,3,4,5};
auto iter = lt.begin();
lt.erase(1);    
cout << *iter << endl;    // 此时的 iter 还有效吗?

所以为了避免使用失效的迭代器,建议在进行可能导致迭代器失效的操作之后,重新获取迭代器来确保迭代器的有效性。

list<int> lt = {1,2,3,4,5};
auto iter = lt.begin();
lt.erase(1);    
iter = lt.begin();    // 重新获取迭代器
cout << *iter << endl;   

三,迭代器的类型萃取(traits)

每个迭代器都会有自己的相应类型(associated types),在上文中我们尝试去设计 list 迭代器的时候就已经见识过了,想要为一个容器设计一个专门的迭代器,就至少会有 value_type、pointer、reference 这三种类型。根据大佬们的经验,迭代器相应类型并不只有“迭代器所指向对象的类型(即 value_type)”,最常用的相应类型有五种,而 value_type、pointer、reference 就只是其中的三种(这里就不详细介绍另外两种了)。

在很多的情况下,我们都需要拿到迭代器的相应类型(在下一部分中我们去实现反向迭代器时就能够体会到了),怎么拿到呢?我们通过一个叫做 iterator_traits 的迭代器特性萃取机来拿到。

template<class Iterator>
struct iterator_traits {
	typedef typename Iterator::value_type	value_type;
	typedef typename Iterator::pointer		pointer;
	typedef typename Iterator::reference	reference;

	// 下面这两种只是提一下, 后文不再出现这两种类型
	typedef typename Iterator::iterator_categoty	iterator_category;
	typedef typename Iterator::difference_type		difference_type;
};

这个萃取机依赖着这样一个规则:凡是一个专门为容器设计的迭代器,都有能力(且因该)定义自己的相应类型,就比如在上文中写过的:

// T 是需要在链表中存放的数据类型, Ref 是引用类型, Ptr 是指针类型
template<class T, class Ref, class Ptr>    
class List_Iterator{
    // ...
    // 为了提高代码的可读性和可维护性, 使得容器的模板参数更具有可定制性和通用性,
    // 这里添加几个 typedef 来定义一下相应的类型, 这个非常重要!!!
    typedef T	value_type;
	typedef Ref reference;
	typedef Ptr pointer;
    // ...
};

但是还有一个特殊的情况,那就是:原生指针。原生指针是一种天然的迭代器,但是它没有能力去定义自己的相应类型。怎么办?我们可以通过模板的特化(partial specialization)来解决这一问题。

// 针对迭代器本身是原生指针类型设计的特化版本
template<class T>
struct iterator_traits<T*> {
	typedef T    value_type;
	typedef T*   pointer;
	typedef T&   reference;
};

// 针对迭代器本身是 const 指针类型设计的特化版本
template<class T>
struct iterator_traits<const T*> {
	typedef T		value_type;
	typedef const	T* pointer;
	typedef const	T& reference;
};

iterator_traits 的示意图:

四,反向迭代器

1,什么是适配器(adapters)

反向迭代器是一种迭代器适配器。适配器(adapters)在STL组件的灵活组合运用功能上,扮演着轴承、转换器的角色。适配器这个概念,事实上是一种设计模式。适配器的定义如下:将一个 class 的接口转换为另一个 class 的接口,使原本因接口不兼容而不能合作的 classes,可以一起运作。

STL 所提供的各种配接器中,改变仿函数(functors)接口者,我们称为函数适配器(function adapter),改变容器(containers)接口者,我们称为容器适配器(container adapter),改变迭代器(iterators)接口者,我们称为迭代器适配器(iterator adapter)。

2,什么是反向迭代器

reverse terator,它的功能就是将迭代器的移动行为倒转。如果 STL 算法接受的不是一般正常的迭代器,而是这种反向迭代器,它就会以从尾到头的方向来处理序列中的元素。例如:

vector<char> vec = { 1,2,3,4,5,6 };
	
find(vec.begin(), vec.end(), 7);    // 从 1 开始查找到 6,  1->2->...->6
find(vec.rbegin(), vec.rend(), 7);  // 从 6 开始查找到 1,  6->5->...->1
// rbegin() 返回反向迭代器的第一个元素, rend() 返回反向迭代器的最后一个元素

我们可以先来看一下两段 vecotr 与 list 的部分源码:

template <class T>
class vector {
public:
	typedef T value_type;
    typedef value_type* iterator;
	typedef reverse_iterator<iterator> reverse_iterator;

	reverse_iterator rbegin() { return reverse_iterator(end()); }
	reverse_terator rend() { return reverse_iterator(begin()); }
    // ...
};


template <class T>
class list {
public:
	typedef __list_iterator<T, T&, T*> iterator;
	typedef reverse_iterator<iterator> reverse_iterator;

	reverse_iterator rbegin() { return reverse_iterator(end()); }
	reverse_terator rend() { return reverse_iterator(begin()); }
    // ...
};

在 STL 的容器中,只要是双向序列容器,就一定支持 rbegin() 与 rend() 操作。

3,反向迭代器的实现

仔细观察一下上面的代码,我们可以发现,反向迭代器的 rbegin(),rend() 分别对应着正向迭代器的 end() 与 begin(),但是 rbegin(),rend() 与 end(),begin() 所指向的并不是同一个元素。如图:

为什么要这么设计呢?这其实主要是为了配合迭代器区间左闭右开的习惯,我们想要将一个正向迭代器区间转换为一个反向迭代器区间的时候不要有任何的额外处理。那么,反向迭代器的内部是怎么实现的呢?

// 反向迭代器类
template<class iterator>
class Reverse_Iterator {
private:
	iterator _it;	// 反向迭代器所对应的正向迭代器

public:
	typedef Reverse_Iterator<iterator> self;

	// 迭代器相应类型萃取
	typedef typename iterator_traits<iterator>::reference	reference;
	typedef typename iterator_traits<iterator>::pointer		pointer;
	typedef typename iterator_traits<iterator>::value_type	value_type;
	

	Reverse_Iterator(iterator it) :_it(it) {}
	Reverse_Iterator(const self& re_it):_it(re_it._it){}

	// 将其他类型的反向迭代器转换为当前类型的反向迭代器
	iterator base()const { return _it; }
	template<class iter>
	Reverse_Iterator(const Reverse_Iterator<iter>& re_it):_it(re_it.base()) {}

	// 下面这个函数就是反向迭代器的关键所在: 对反向迭代器取值
	// 就是将其对应的正向迭代器后退一个之后再取值
	reference operator*()const { 
		iterator tmp = _it;
		return *(--tmp);
	}
	pointer operator->()const { return  &(operator*()); }

	// ++ 实际上变成 --
	self operator++() { return --_it; }
	self operator++(int) {
		iterator tmp = _it;
		--_it;
		return tmp;
	}

	// -- 实际上变成 ++
	self operator--() { return ++_it; }
	self operator--(int) {
		iterator tmp = _it;
		++_it;
		return tmp;
	}

	bool operator==(const self& rit) { return _it == rit._it; }
	bool operator!=(const self& rit) { return not (_it == rit._it); }
    // ...
};

对反向迭代器进行 ++ 与 -- 的示意图:

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

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

相关文章

动态多态的注意事项

大家好&#xff1a; 衷心希望各位点赞。 您的问题请留在评论区&#xff0c;我会及时回答。 多态的基本概念 多态是C面向对象三大特性之一&#xff08;多态、继承、封装&#xff09; 多态分为两类&#xff1a; 静态多态&#xff1a;函数重载和运算符重载属于静态多态&#x…

程序员35岁会失业吗?这事儿其实没那么简单!

35岁被认为是程序员职业生涯的分水岭&#xff0c;许多程序员开始担忧自己的职业发展是否会受到年龄的限制。有人担心随着年龄的增长&#xff0c;技术更新换代的速度会使得资深程序员难以跟上&#xff1b;而另一些人则认为&#xff0c;丰富的经验和深厚的技术积累是年轻程序员无…

基于WTVXXX-32SS智能门锁后板锁方案

一&#xff1a;简介 随着用户认知和生活习惯的改变&#xff0c;消费者对构建便捷生活和智能家居系统的诉求持续增多&#xff0c;智能门锁作为智能家居的门面和典型的物理级入口&#xff0c;成为打造全屋智能必不可少的环节。随着智能门锁行业规模的不断提升&#xff0c;产品的生…

2024南京人工智能展会:定于2024年11月份在南京国际博览中心举行

2024南京国际人工智能展览会&#xff0c;拟定于2024年11月份在南京国际博览中心隆重召开。这一盛大的科技盛宴&#xff0c;无疑将为全球人工智能领域注入新的活力&#xff0c;推动科技创新与社会进步。 此次展览会将以“智能未来&#xff0c;共创辉煌”为主题&#xff0c;汇聚全…

IDEA中快速配置Git

Git介绍&#xff1a; Git下载 idea中配置Git

淘宝app商品数据API接口|item_get_app-获得淘宝app商品详情原数据

获得淘宝app商品详情原数据 API返回值说明 item_get_app-获得淘宝app商品详情原数据 公共参数​​​​​​ 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地…

低代码开发平台开源:提升效率,轻而易举!

当前&#xff0c;数字化转型与社会高速发展都是人们肉眼可见的发展趋势。作为中小企业&#xff0c;如何在激烈的市场竞争中脱颖而出&#xff1f;如何赢得话语权和主动权&#xff0c;从而提升市场竞争力&#xff1f;这就需要考虑引进更为先进和专业的办公利器了。低代码开发平台…

【Java程序设计】【C00379】基于(JavaWeb)Springboot的旅游服务平台(有论文)

【C00379】基于&#xff08;JavaWeb&#xff09;Springboot的旅游服务平台&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;已经做了六年的毕业设计程序开发&#xff0c…

网游版五子棋

五子棋游戏属于开房间类休闲游戏。可以非常方便实现分布式战斗服横向拓展&#xff0c;只要感觉服务器有压力&#xff0c;可以通过动态加战斗服服务器来实现。本文介绍一个基于jforgame组件开发的五子棋网络小游戏&#xff0c;支持分布式部署战斗服。 1.通信组件 浏览器&#…

python IDLE shell 编辑多行代码

1 打开软件 2创建python文本编辑 创建项目 点击File显示如图 再点击New File 将会弹出文本编辑区域 编辑多行代码 def greet(name):print("Hello, " name "!")greet("World")保存 点击File 再点击 Save 即可保存 &#xff08;或…

【动态规划】【数学方法】Leetcode 343. 整数拆分

【动态规划】【数学方法】Leetcode 343. 整数拆分 解法 动态规划解法 数学 每次拆成n个3&#xff0c;如果剩下是4&#xff0c;则保留4&#xff0c;然后相乘 ---------------&#x1f388;&#x1f388;343. 整数拆分 题目链接&#x1f388;&#x1f388;------------------- …

创建多节点 k8s 集群

主机IP系统master192.168.2.15ubuntu20.04 x64 2C 4GWorker1192.168.2.16ubuntu20.04 x64 2C 4GWorker1192.168.2.18ubuntu20.04 x64 2C 4G 使用 iterm2 连接四台服务器 command shift i 同时操作 初始化配置 关闭防火墙 systemctl stop firewalld systemctl disable firewa…

【学习】软件测试中,我们如何有效地跟踪和管理缺陷?

在软件测试中&#xff0c;如何有效地跟踪和管理缺陷&#xff1f;别急&#xff0c;一起来看下小编今日带来的分享。 1.缺陷报告 建立一个缺陷报告系统&#xff0c;让用户和团队成员能够提交缺陷报告。确保缺陷报告中包括清晰的问题描述、重现步骤、预期结果和实际结果等信息。2…

数组的概述

数组的概述 为什么需要数组 需求分析1&#xff1a; 需要统计某公司50个员工的工资情况&#xff0c;例如计算平均工资、找到最高工资等。用之前知识&#xff0c;首先需要声明50个变量来分别记录每位员工的工资&#xff0c;这样会很麻烦。因此我们可以将所有的数据全部存储到一…

java使用阿里巴巴oss

一 .准备 进入阿里 进入控制台 创建bucket 新建目录 点击AccessKey管理 创建AccessKey并复制下载key值 二.使用 导入阿里巴巴和spring依赖 <dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version&…

进程管理命令

进程的概念 进程:运行中的程序(过程,动态) 程序:存储在磁盘上的二进制可执行文件;(静态) 操作系统是通过管理进程,让进程运行来完成用户的任务的; PCB:进程控制块,记录的是进程的相关属性信息;PID:是操作系统对进程的标识;唯一的; 简而言之, 程序:指令数据; 进程:运行中的程序…

Linux:进程概念认识

进程 基本概念 课本概念&#xff1a;程序的一个执行实例&#xff0c;正在执行的程序等 内核观点&#xff1a;担当分配系统资源&#xff08; CPU 时间&#xff0c;内存&#xff09;的实体。 描述进程 -PCB 进程信息被放在一个叫做进程控制块的数据结构中&#xff0c;可以理解为…

前端必会的一些基础

1、如何把obj对象 添加到arr数组对象内 2、手机号、邮箱、隐藏用户手机号中间四位正则 3、两个数组 数组a未全部人员 数组b为已选中人员 默认选中 4、数组去重、 5、localStorage 存取 数组 方法 6、数据filter过滤 7、请求接口时header 请求格式不对 需要怎么转换&#xf…

缺省和重载——初识c++

. 个人主页&#xff1a;晓风飞 专栏&#xff1a;数据结构|Linux|C语言 路漫漫其修远兮&#xff0c;吾将上下而求索 文章目录 C输入&输出cout 和cin<<>> 缺省参数全缺省半缺省应用场景声明和定义分离的情况 函数重载1.参数的类型不同2.参数的个数不同3.参数的顺…

C++运算符重载中的引用返回

文章目录 引言原因1.为了支持链式调用2.避免不必要的对象创建和复制3.保持语义一致性 引言 在C编程语言中&#xff0c;运算符重载是一项强大的特性&#xff0c;它允许程序员为自定义类型重新定义或重载已有的运算符&#xff0c;从而使得这些类型能够像内置类型一样使用运算符。…