简介
迭代器是一种用于遍历容器元素的对象。它提供了一种统一的访问方式,使程序员可以对容器中的元素进行逐个访问和操作,而不需要了解容器的内部实现细节。
C++标准库里每个容器都定义了迭代器,这迭代器的名字就叫容器迭代器
迭代器的作用类似于指针,可以指向容器中的某个元素,并通过操作迭代器来访问和操作该元素。通过迭代器,我们可以实现对容器的遍历、查找、修改等操作,大大增强了程序的灵活性和通用性。
声明正向迭代器
有迭代器的容器类型使用iterator和const_iterator类型来表示正向迭代器的类型
(下面我们会讲到反向迭代器,它的类型是reverse_iterator或者const_reverse_iterator)
我们口语中的迭代器多数指的是正向迭代器
我们可以看个例子
vector<int>::iterator it1;
//it1能读取和修改vector<int>的元素
string::iterator it2;
//it2能读取和修改string的元素
vector<int>::const_iterator it3;
//it3能读取vector<int>的元素,不能修改string的元素
string::const_iterator it4;
//it4能读取string的元素,不能修改string的元素
const_iterator的对象和常量指针差不多,能读取但是不能修改它所指元素的值。相反,iterator的对象可读可写。
如果vector和string对象是个常量,只能使用const_iterator;
如果vector和string对象不是常量,则既可以使用iterator也可以使用const_iterator
迭代器范围
迭代器范围的概念是标准库的基础。
一个迭代器范围(iterator range)由一对迭代器表示,两个迭代器分别指向同一个客器中的元素或者是尾元素之后的位置(one past the last element)。
这两个迭代器通常被称为begin和end,或者是first和last(可能有些误导),它们标记了容器中元素的个范围。
虽然第二个迭代器常常被称为last,但这种叫法有些误导,因为第二个迭代器从来都不会指向范围中的最后一个元素,而是指向尾元素之后的位置。
迭代器范围中的元素包含first所表示的元素以及从first开始直至last(但不包含last)之间的所有元素。
这种元素范围被称为左闭合区间(left-inclusive interval),其标准数学描述为
[begin, end)
表示范围自begin开始,于end之前结束。迭代器begin和end必须指向相同的容器。end可以与begin指向相同的位置,但不能指向begin之前的位置。
对构成范围的迭代器的要求
如果满足如下条件,两个迭代器begin和end构成一个迭代器范围:
- 它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置
- 我们可以通过反复递增begin来到达end。换句话说,end 不在begin之前。
- 编译器不会强制这些要求。确保程序符合这些约定是程序员的责任。
使用左闭合范围蕴含的编程假定
标准库使用左闭合范围是因为这种范围有三种方便的性质。
假定begin和end 构成一个合法的迭代器范围,则
- 如果begin与end相等,则范围为空
- 如果 begin 与end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
- 我们可以对begin递增若干次,使得begin==end
这些性质意味着我们可以像下面的代码一样用一个循环来处理一个元素范围,而这是
安全的:
while (begin != end)
*begin = val;// 正确:范围非空,因此begin指向一个元素
//移动迭代器,获取下一个元素
++begin;
给定构成一个合法范围的迭代器begin和end,若begin==end,则范围为空。在此情况下,我们应该退出循环。如果范围不为空,begin指向此非空范围的一个元素。因此,在while循环体中,可以安全地解引用begin,因为begin必然指向一个元素。最后,由于每次循环对beain递增一次,我们确定循环最终会结束。
正向迭代器(iterator和const_iterator)
begin()和end()
begin()返回指向第一个元素(或第一个字符)的迭代器。如有下述语句:
//由编译器决定b和e的类型,我们下面会讲
//b表示v的第一个元素,e表示v尾元素的下一位置
auto b = v.begin(), e=v.end();//b 和e的类型相同
end成员则负责返回指向容器尾元素的下一位置的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后”元素。这样的迭代器没什么实际含义,仅是个标记而已,表示我们已经处理完了容器中的所有元素。end成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。
特殊情况下如果容器为空,则begin和end返回的是同一个选代器。
一般来说,我们不清楚(不在意)迭代器准确的类型到底是什么。在上面的例子中,使用 auto关键字定义变量b和e,这两个变量的类型也就是begin和end的返回值类型,我们后面将对相关内容做更详细的介绍。
cbegin()和cend()
cbegin()
和cend()
是在C++中用于迭代器的函数。
cbegin()
函数返回一个常量迭代器,它指向容器的第一个元素。常量迭代器意味着不能通过该迭代器来修改容器中的元素。
cend()
函数返回一个常量迭代器,它指向容器的最后一个元素的下一个位置。由于它指向最后一个元素的下一个位置,因此不能通过该迭代器访问容器中的元素。
这两个函数主要用于在循环中遍历容器的元素。使用常量迭代器可以确保不会意外修改容器的内容,从而提高代码的安全性。
这两个和begin()和end()其实差不多,只是cbegin()和cend()的返回值一定是const_iterator类型,begin()和end()的不一定是const_iterator,还可以是iterator,这个我们下面会讲
反向迭代器(reverse_iterator和const_reverse_iterator)
上面我们讲的是普通迭代器,接下来我们要讲反向迭代器
首先,反向迭代器类型是reverse_iterator或者const_reverse_iterator
反向迭代器就是在容器中从尾元素向首元素反向移动的选代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向选代器(++it)会移动到前一个元素;递减一个迭代器(--it)会移动到下一个元素。
除了forward_list之外,其他容器都支持反向迭代器。
我们可以通过调用rbegin,rend、crbegin和crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾无素和首元素之前一个位置的迭代器。与普通迭代器一样,反向选代器也有const版本和非const版本。
下面的循环是一个使用反向迭代器的例子,它按逆序打印vec中的元素:
vector<int> vec =(0,1,2,3,4,5,6,7,8,9);
//从尾元素到首元素的反向迭代器
for (auto r_iter = vec.crbegin(); //将r iter绑定到尾元素
r_iter != vec.crend(); // crend指向首元素之前的位置
++r_iter) // 实际是递减,移动到前一个元素
cout << *r_iter << endl; // 打印 9,8, 7,... 0
虽然颠倒递增和递减运算符的含义可能看起来令人混淆,但这样做使我们可以用算法透明地向前或向后处理容器。
例如,可以通过向sort传递一对反向迭代器来将vector整理为递减序:
sort(vec.begin(), vec.end());//按“正常序”排序vec
//按逆序排序:将最小元素放在vec的末尾
sort(vec.rbegin(), vec.rend());
反向迭代器需要递减运算符
不必惊讶,我们只能从既支持++也支持--的迭代器来定义反向迭代器。毕竟反向迭代器的目的是在序列中反向移动。
除了forward_list之外,标准容器上的其他迭代器都既支持递增运算又支持递减运算。
但是,流迭代器不支持递减运算,因为不可能在一个流中反向移动。
因此,不可能从一个forward_list或一个流迭代器创建反向选代器。
反向迭代器的注意点
假定有一个名为line的string,保存着一个逗号分隔的单词列表,我们希望打印line中的第一个单词。使用find可以很容易地完成这一任务:
//在一个逗号分隔的列表中查找第一个元素
auto comma =find(line.cbegin(), line.cend(),',');
cout << string(line.cbegin(), comma) << endl;
如果line中有逗号,那么comma将指向这个逗号;否则,它将等于line.cend()。
当我们打印从line.cbegin()到comma之间的内容时,将打印到逗号为止的字符,或者行印整个string(如果其中不含逗号的话)。
如果希望打印最后一个单词,可以改用反向迭代器:
//在一个逗号分隔的列表中查找最后一个元素
auto rcomma = find(line,crbegin(), line.crend(),',');
由于我们将crbegin()和crend()传递给 find, find将从line的最后一个字符开始向前搜索。当find完成后,如果line中有逗号,则rcomma指向最后一个逗号——即,它指向反向搜索中找到的第一个逗号。如果 line 中没有逗号,则 rcomma 指向line.crend()。
当我们试图打印找到的单词时,最有意思的部分就来了。看起来下面的代码是显然的方法
string line = "FIRST,MIDDLE,LAST";
auto rcomma = find(line.crbegin(), line.crend(), ',');
// 错误:将逆序输出单词的字符
cout << string(line.crbegin(), rcomma) << endl;
但它会生成的输出结果和我们预期的不同哦。
这是因为我们使用的是反向迭代器,会反向处理string,因此,上述输出语句从 crbegin 开始反向打印line中内容。而我们希望按正常顺序打印从rcomma 开始到line末尾间的字符。所以我们不能直接使用rcomma。因为它是一个反向迭代器,意味着它会反向朝着string的开始位置移动。
那我们怎么按正常顺序打印最后一个单词呢?
需要做的是,将rcomma转换回一个普通迭代器,能在line 中正向移动。我们通过调用reverse_iterator的base成员函数来完成这一转换,此成员函数会返回其对应的普通迭代器:
string line = "FIRST,MIDDLE,LAST";
auto rcomma = find(line. crbegin(), line.crend(),',');
//正确:得到一个正向选代器,从逗号开始读取字符直到line末尾
cout << string(rcomma. base(), line.cend()) << endl;
给定和之前一样的输入,这条语句会如我们的预期打印出LAST。
反向迭代器和普通迭代器的关系
图10.2中的对象显示了普通迭代器与反向迭代器之间的关系。
例如,rcomma 和rcomma.base()指向不同的元素,line.crbegin和line.cend()也是如此。这些不同保证了元素范围无论是正向处理还是反向处理都是相同的。
从技术上讲,普通迭代器与反向迭代器的关系反映了左闭合区间的特性。
关键点在于[line.crbegin(),rcomma)和[rcomma.base(),line.cend())指向line中相同的元素范围。
为了实现这一点,rcomma和rcomma.base()必须生成相邻位置而不是相同位置,crbegin()和cend()也是如此。
反向迭代器的目的是表示元素范围,而这些范围是不对称的,这导致一个重要的结果:当我们从一个普通迭代器初始化一个反向迭代器,或是给一个反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同的元素
begin成员和end成员的返回值
begin和end 操作生成指向容器中第一个元素和尾元素之后位置的迭代器。
这两个迭代器最常见的用途是形成一个包含容器中所有元素的迭代器范围。
begin和end有多个版本:带r的版本返回反向迭代器;以c开头的版本则返回const迭代器:
list<string> a ={"Milton", "Shakespeare", "Austen"};
auto itl = a.begin(); // list<string>::iterator
auto it2 = a.rbegin(); // list<string>::reverse iterator
auto it3 = a.cbegin(); // list<string>::const iterator
auto it4 = a.crbegin();// list<string>::const reverse iterator
不以c开头的begin和end成员
不以c开头的函数都是被重载过的。
也就是说,实际上begin(),end(),rbegin(),rend()都有两个版本。
一个是const成员,返回容器的const iterator类型。
另一个是非常量成员,返回容器的iterator类型。
那它什么时候调用哪个呢?
不以c开头的begin和end运算符的返回类型取决于调用它的这个对象是否是常量
如果对象是常量,begin和end返回const_iterator,如果对象不是常量,begin和end返回iterator
如果对象是常量,rbegin和rend返回const_reverse_iterator,如果对象不是常量,rbegin和rend返回reverse_iterator.
vector<int> a;
const vector<int> cv;
auto it1=v.begin();
auto it2=cv.begin();
auto it3=v.rbegin();
auto it4=cv.rbegin();
我们可以看到it1的类型是vector<int>::iterator,it2的类型是vector<int>::const iterator
it3的类型是vector<int>::reverse_iterator;it4的类型是vector<int>::const_reverse_iterator
rbegin、begin(),end和rend的情况类似。当我们对一个非常量对象调用这些成员时,得到的是返回iterator的版本。只有在对一个const对象调用这些函数时,才会得到一个const版本。
以c开头的begin和end成员
以c开头的begin和end成员只有一种版本,它只会返回const_iterator类型
也就是说cbegin()和cend()只会返回const_iterator类型
crbegin()和crend()只会返回const_reverse_iterator类型。
我们看看
vector<int> v;
auto it5=v.cbegin();
auto it6=v.crbegin();
可以看到啊,it5是const_iterator类型,it6是const_reverse_iterator类型
crend(),crbegin(),cbegin(),cend()的情况类似。只会得到const_iterator.
与const指针和引用类似可以将一个普通的iterator转换为对应的const_iterator,但反之不行。
话不多说,我们直接看例子
vector<int> vec = { 1, 2, 3, 4, 5 };
vector<int>::iterator a = vec.begin();
vector<int>::const_iterator b = a;//这是可以的
vector<int> vec = { 1, 2, 3, 4, 5 };
vector<int>::const_iterator a = vec.begin();
vector<int>::iterator b = a;//这是不可以的
普通迭代器(正向迭代器)和反向迭代器的关系
反向迭代器背后的原理是依靠正向迭代器创建出来的,
也就是说正向迭代器支持的运算操作,反向迭代器也支持,只不过操作的效果是相反的
迭代器运算符
在C++中,迭代器提供了一些运算符来对迭代器进行操作和访问容器中的元素。以下是常用的迭代器运算符:
-
解引用运算符(*):用于获取迭代器指向位置的元素值。例如,*it 表示获取迭代器 it 指向位置的元素值。
-
自增运算符(++):用于将迭代器向前移动一个位置。例如,++it表示将迭代器 it 向前移动一个位置。
-
自减运算符(--):用于将迭代器向后移动一个位置。例如,--it表示将迭代器 it 向后移动一个位置。
-
箭头运算符(->):用于获取迭代器指向位置的成员变量或成员函数。例如,it->member 表示获取迭代器 it 指向位置的成员变量或成员函数。
-
等于运算符(==)和不等于运算符(!=):用于比较两个迭代器是否指向同一个位置。例如,it1 == it2 表示判断迭代器 it1 和 it2 是否指向同一个位置。
这些是所有容器迭代器都支持的操作,其中有一个例外不符合公共接口特点——forward_list迭代器不支持递减运算符(--)。
迭代器的算术运算
这些只能用于string,vector,deque,array的迭代器,我们不能将它们用于其他任何容器类型的迭代器