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

news2025/1/9 0:04:04

目录

一,什么是迭代器

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); }
    // ...
};

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

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

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

相关文章

稀碎从零算法笔记Day28-LeetCode:零钱兑换

前言&#xff1a;鸽了好多天了哈哈哈&#xff0c;虽然C站没更但是LC还是坚持刷的&#xff0c;任重道远啊&#xff01;(可恶的寝室熄灯) 题型&#xff1a;动态规划 链接&#xff1a;322. 零钱兑换 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述…

城市内涝排水系统最新模型:慧天【HTWATER】与SWMM完美耦合

查看原文>>> 城市内涝水文水动力模型&#xff1a;慧天【HTWATER】 【城市内涝水文水动力模型介绍】 慧天排水数字化分析平台针对城市排水系统基础设施数据管理的需求&#xff0c;以及水文、水力及水质模拟对数据的需求&#xff0c;实现了以数据库方式对相应数据的存…

静态资源动态访问的细节补充import()与new URL

教学视频的补充 静态资源的动态访问【渡一教育】_哔哩哔哩_bilibili import里若用引号拼接,那么打包结果不会有图片 细节1:得用es6的反单引号才可以 细节2:不能以变量结尾 var p 12 //错误import(./assets/ p .jpg).then(res > {console.log(res, res);}) //正确var p…

201812 CSP认证 | CIDR合并

CIDR合并 难是真的不难但是也写了我几个小时服了 这道题在有计网的基础上就很好理解了&#xff0c;没有在格式上有任何刁难你的。这里不讲背景了 官网提交结果以及满分代码如下&#xff1a; #include<bits/stdc.h> using namespace std; typedef long long ll; typedef…

Qt Design Studio 软件怎么用(详细+通俗+有趣)

建议&#xff1a;本文长期更新&#xff0c;建议点赞/收藏&#xff01; 1. 啥是Qt Design Studio&#xff1f; Qt Design Studio 是一个用于设计和开发用户界面的工具&#xff0c;特别适合开发跨平台应用程序。它结合了UI设计和开发的工作流程&#xff0c;使得设计师和开发者可…

​奶茶店小程序有哪些功能

​奶茶店作为饮品行业的一种热门经营模式&#xff0c;拥有自己的小程序可以带来诸多便利和增长机会。在这篇文章中&#xff0c;我们将探讨奶茶店小程序应该具备的功能&#xff0c;以满足消费者的需求&#xff0c;提升店铺的运营效率和用户体验。 1. **在线订购功能**&#xff…

linux文本三剑客 --- grep、sed、awk

1、grep grep&#xff1a;使用正则表达式搜索文本&#xff0c;将匹配的行打印出来&#xff08;匹配到的标红&#xff09; 命令格式&#xff1a;grep [option] pattern file <1> 命令参数 -A<显示行数>&#xff1a;除了显示符合范本样式的那一列之外&#xff0c;并…

Windows/Mac 上的 7 个顶级数据恢复软件榜单

数据恢复软件可帮助您从众多存储设备中恢复损坏或删除的数据。该文件恢复软件可以恢复文件、文档、视频、照片等。这些应用程序支持许多标准文件格式&#xff0c;如 PNG、RTF、PDF、HTML、JPG、MP3 等。以下是具有流行功能的顶级数据恢复软件列表和网站链接。磁盘恢复软件列表包…

【Python系列】Python 中 YAML 文件与字典合并的实用技巧

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

真假难辨:凯特王妃最新视频疑似AI合成,公众哗然!

会议之眼 快讯 北京时间3月23日凌晨&#xff0c;英国的凯特王妃发布了一段震撼人心的视频声明&#xff0c;坦诚自己正在面临一场艰难的挑战——患上了癌症。她坦言目前正在接受预防性化疗&#xff0c;并处于治疗的“早期阶段”&#xff0c;这一消息是在她暂时退出公众生活两个多…

智慧城市一屏统览,数字孪生综合治理

现代城市作为一个复杂系统&#xff0c;牵一发而动全身&#xff0c;城市化进程中产生新的矛盾和社会问题都会影响整个城市系统的正常运转。智慧城市是应对这些问题的策略之一。城市工作要树立系统思维&#xff0c;从构成城市诸多要素、结构、功能等方面入手&#xff0c;系统推进…

力扣Lc21--- 389. 找不同(java版)-2024年3月26日

1.题目描述 2.知识点 &#xff08;1&#xff09;在这段代码中&#xff1a; // 统计字符串s中每个字符的出现次数for (int i 0; i < s.length(); i) {count[s.charAt(i) - a];}对于字符串s “abcd”&#xff1a; 当 i 0&#xff0c;s.charAt(i) ‘a’&#xff0c;ASCII…

Kali开启远程服务

一&#xff0c;先切换root账户 二、kali开启远程服务 1&#xff0c;修改远程登录的配置文件 vim /etc/ssh/sshd_config &#xff08;用文本编辑器打开此文件) 在文件的普通模式下&#xff0c;使用/PermitRootLogin&#xff0c;回车&#xff0c;查找到该行&#xff0c;i&#…

鸿蒙OS开发实例:【demo选择列表限定数量】

效果图&#xff1a; 示例代码 // 使用 DevEco Studio 3.1.1 Release 及以上版本&#xff0c;API 版本为 api 9 及以上。 // 主要功能及注意事项&#xff1a; // 该组件展示了一个乘客选择列表。列表中的每个项目包含一个复选框和对应的乘客姓名&#xff0c; // 用户点击任意一…

Redis如何应对缓存穿透问题——Java全栈知识(9)

我们在正常使用缓存的时候的流程大概就是这样的&#xff1a; 请求访问缓存&#xff0c;缓存有数据就返回&#xff0c;缓存无数据就去数据库里面查数据写入到缓存中。 1、缓存穿透问题 但是如果由恶意请求&#xff0c;短时间内大量的访问不存在的数据&#xff0c;这时每个请求…

工作中遇到的疑难杂症,以及解决办法

一 pagehelper和自定义mybatis拦截器冲突bug 1.1 问题描述 当在springboot工程&#xff0c;持久层使用mybatis&#xff0c;且使用pagehelper实现分页功能&#xff0c;当随着业务发展&#xff0c;需要自定义一个mybatis的拦截器实现sql的打印&#xff0c;但是出现拦截器失效&…

flac和mp3可以互相转换吗?flac和mp3互相转换的方法

一&#xff0c;什么是FLAC格式 FLAC即是Free Lossless Audio Codec的缩写&#xff0c;中文可解为无损音频压缩编码。FLAC是一套著名的自由音频压缩编码&#xff0c;其特点是无损压缩。不同于其他有损压缩编码如MP3 及 AAC&#xff0c;它不会破坏任何原有的音频资讯&#xff0c…

【保姆级教程】YOLOv8自动数据标注

一、YOLOV8环境准备 1.1 下载安装最新的YOLOv8代码 仓库地址&#xff1a; https://github.com/ultralytics/ultralytics1.2 配置环境 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple1.3 安装labelme标注工具 pip install labelme二、半自动标注…

MATLAB 自定义生成直线点云(详细介绍) (47)

MATLAB 自定义生成直线点云 (详细介绍)(47) 一、算法介绍二、具体步骤二、算法实现1.代码2.效果一、算法介绍 通过这里的直线生成方法,可以生成模拟直线的点云数据,并通过调整起点、终点、数量和噪声水平等参数来探索不同类型的直线数据。这种方法可以用于测试、验证和开…

【考研数学】跟武忠祥,如何使用好《复习全书》和《高数辅导讲义》?

本人就是全程跟武忠祥老师高分上岸的&#xff01; 高数辅导讲义建议在强化阶段再买就行 在基础阶段把基础篇吃透就ok 本人属于基础很差相当于是零基础的考研党&#xff0c;经过一年备考成功上岸 中间花费了很多时间在考研数学备考信息检索上&#xff0c;写下这篇希望能帮助…