【C++】C++中的迭代器

时间:2023-12-25 21:57:37

目录结构:

contents structure [-]

迭代器类型类似于指针类型,也提供了对对象的间接访问。就迭代器而言,其对象便是容器中的元素或者string对象中的字符。使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另一个元素。迭代器有有效和无效之分,这一点和指针差不多。有效的迭代器指向某个元素,或者指向容器中尾元素的下一位置。

1 迭代器的运算

1.1 迭代器的运算符

下面列举了一些普遍迭代器都支持的运算符:

运行符‘ 说明
*iter 返回迭代器iter所指元素的引用
iter->men 解引用iter并获取改元素的名为men的成员,等价于(*iter).men
++iter 令iter指示容器中的下一个元素
--iter 令iter指示容器中的上一个元素
iter1 == iter2 判断两个迭代器是否相等,若两个迭代器指向同一个元素或者他们是同一个容器的尾后迭代器,则相等;反之,不相等。
iter1 != iter2 判断两个迭代器是否不相等。
iter + n 迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内一个元素,或者指示容器尾元素的下一位置
iter - n 迭代器减去一个整数值仍得到一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个元素。结果迭代器或者指示容器内一个元素,或者指示容器尾元素的下以位置
iter += n 等同于 iter = iter + n
iter -= n 等同于 iter = iter - n
iter1 - iter2 两个迭代器相减的结果就是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动若干个元素后就得到左侧的迭代器了。参与运算的两个迭代器必须指向同一个容器中的元素或者尾容器的下一个元素。
>,>=,<,<= 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说明前者小于后者。参与运算的两个迭代器必须指向同一个容器中的元素或者尾元素的下一元素。

并不是所有的迭代器都支持上面这些运算符,关键是要看迭代器是否定义了相应的操作符。

下面使用迭代器把string中的第一个字母改成了大写形式:

string s("some string");
if(s.begin() != s.end()){//确保s不为空
auto it = s.begin(); //it表示s第一个字符的大写字母
*t = toupper(*t); //将当前字母改成大写形式
}

迭代器可以使用++运算符来从一个元素移动到另一个元素。从逻辑上讲,迭代器的递增和整数的递增类似,整数的递增是在整数值上加1,迭代器的递增怎是将迭代器“向前移动一个位置”。下面使用这个特性,也可以实现上面的效果:

for(auto it=s.begin(); it!=s.end() && !isspace(*it); ++it)
*it = toupper(*it);

解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员(迭代器和指针类似,所以这一规则同样适用与指针)。
例如,对于一个由一串字符串组成的vector对象来说,想要检查其元素是否为空,令it是为iterator对象的迭代器,只需要检查it所指字符串是否为空就可以了,其代码如下:

(*it).empty(); //解引用it,然后调用结果对象的empty成员。
*it.empty();//错误:试图访问it名为empty()的成员,但it是一个迭代器,没有empty()的成员

为了简化这种操作,c++语言提供了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起:

it->empty(); //等同于(*it).empty();

1.2 begin和end操作符

//依次输出text的每一行直至遇到第一个空白符为止
vector<string> text{"abc","def"};
for(auto it=text.begin(); it!=text.end() && !it->empty(); ++it)//使用it->empty()判断所指是否为空
cout << *it << endl;

指针和迭代器类似,也可以操作使用->运算符,例如:

string s1 = "a string", *p = &s1;
auto n = s1.size();//运行string对象s1的size成员
n = (*p).size();//运行p所值对象的size成员
n = p->size();//等价于(*p).size();

2 迭代器的类型有那些

C++的标准库中定义了5种迭代器类型(C++17及其之后有6种迭代器类型)。这六大类迭代器分别是:LegacyInputIterator(输入迭代器), LegacyOutputIterator(输出迭代器), LegacyForwardIterator(前向迭代器), LegacyBidirectionalIterator(双向迭代器), LegacyRandomAccessIterator(随机访问迭代器), and LegacyContiguousIterator(相接迭代器)。

迭代器是按照它们所提供的操作来分类的,这意味着只要有支持迭代器操作符的类型就可以被用作迭代器,例如:指针支持LegacyRandomAccessIterator迭代器支持的所有的操作,因此指针可以被用在任何可以使用LegacyRandomAccessIterator的地方。

所有的迭代器都具有层次关系(除LegacyOutputIterator和LegacyContiguousIterator),高层类别的迭代器支持低层类别迭代器的所有操作。如果在这个层次列表中的任何一个迭代器又支持LegacyOutputIterator迭代器的话,那么这个迭代器被称为可变迭代器(mutable iterator),否则被称为不可以变迭代器(Non-mutable iterators)或者常量迭代器(constant iterator)。
【C++】C++中的迭代器

输入迭代器(LegacyInputIterator):可以用于读取序列中的元素,输入迭代器只用于顺序访问,而且是单向的。一旦一个输入迭代器增加一个元素,那么它之前所有的复制值都可能无效。

前向迭代器(LegacyForwardIterator):可以用于读写元素,前向迭代器只能用于顺序访问,而且是单向的。前向迭代器支持对序列的多遍扫描,forward_list容器上的迭代器都是前向迭代器。

双向迭代器(LegacyBidirectionalIterator):可以用于读写元素,双向迭代器支持对序列的双向顺序访问。支持对序列的多遍扫码,除了forward_list之外,其他标准库都提供符合双向迭代器要求的迭代器。

随机访问迭代器(LegacyRandomAccessIterator):提供了在常量时间内对序列中任何元素的访问的能力,它支持双向迭代器的所有功能。

输出迭代器(LegacyOutputIterator):可以用于对序列中元素的写入操作,输出迭代器是顺序访问,而且是单向的。当一个输出迭代器,满足LegacyRandomAccessIterator、LegacyBidirectionalIterator、LegacyForwardIterator中的任何一个迭代器时,它被称为可变迭代器(mutable iterator)。

相接迭代器(LegacyContiguousIterator):相接迭代器是C++17新添加的一种迭代器,它的元素在逻辑上相邻以及在内存物理地址上相邻。例如:指向数组中某个元素的指针就完全符合相邻迭代器的要求。

3 常用迭代器

3.1 容器的迭代器

C++中的容器都为自己的类型定义了一种迭代器。例如string,vector,map,list等等。

这类迭代器和它的容器高度耦合。接下来,展示一些迭代器的使用:

#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <algorithm> /*for_each*/ using namespace std; int main(int argc,char* argv[]){
string str = "abc";
string::iterator iter1 = str.begin();
while(iter1 != str.end()){
cout << *iter1 << " ";
iter1++;
}
cout << "\n"; vector<int> vec{,,,,};
auto iter2 = vec.begin();//iter2 是vector<int>::iterator类型
for_each(iter2,vec.end(),[](const int i){
cout << i << " ";
});
cout << "\n"; list<string> lt{"hello","word"};
list<string>::const_iterator iter3 = lt.cbegin();
while(iter3 != lt.cend()){
cout << *(iter3++) << " ";
}
cout << endl;
return ;
}

输出:

a b c
1 2 3 4 5
hello word

3.2 插入迭代器

插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。

插入器有三种,差异表现在插入的位置:

back_inserter 创建一个使用push_back的迭代器。
front_inserter 创建一个使用push_front的迭代器。
inserter 创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

back_inserter函数会得到一个back_insert_iterator迭代器对象,它定义在<iterator>头文件中,back_insert_iterator属于输出迭代器,back_insert_iterator会调用容器的push_back插入元素。应此,back_inserter函数接受的容器类型,必需具有push_back函数。back_insert_iterator总是插入元素到容器的末尾。

front_inserter函数会得到一个front_insert_iterator迭代器对象,它定义在<iterator>头文件中,front_insert_iterator属于输出迭代器,front_insert_iterator会调用容器的push_front插入元素。应此,front_inserter函数接受的容器类型,必需具有push_front函数。front_insert_iterator总是插入元素到容器的开端。

inserter函数会得到一个insert_iterator迭代器对象,它定义在<iterator>头文件中,insert_iterator属于输出迭代器,insert_iterator会调用容器的insert插入元素。应此,inserter函数接受的容器类型,必需具有insert函数。insert_iterator总是插入元素到指向的元素之前。

理解插入器的工作机制非常重要:当调用inserter(c,iter)时,我们得到一个迭代器,接下来使用它时,会将元素插入到iter原来所指向的元素之前的位置。即,如果it是由inserter生成的迭代器,则下面的赋值语句:

*it = val;

其效果与下面的代码是一样的

it = c.insert(it,val); //it指向c容器中新加入的元素
++it; //递增it使它指向原来的元素

back_inserter,front_inserter,inserter这三种插入适配器生成的迭代器完成不一样。

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm> int main()
{
std::vector<int> v{};
std::back_insert_iterator<std::vector<int>> iter = std::back_insert_iterator<std::vector<int>> (v);
iter = ;// iter包含1 10
iter = ;// iter包含1 10 20 std::list<std::int> lst = {,,,};
std::list<std::int> lst2,lst3; //空list
//拷贝完成后,lst2包含4 3 2 1
copy(lst.cbegin(),lst.cend(),front_inserter(lst2));
//拷贝完成后,lst3包含1 2 3 4
copy(lst.cbegin(),lst.cend(),inserter(lst3,lst3.begin()));
}

当我们传递给inserter的位置原来指向第一个元素,只要我们在此元素之前插入一个新元素,此元素就不再是容器的首元素了。

3.3 流迭代器

虽然iostream类型不是容器,但标准库定义了可以用于这些IO类型对象的迭代器。istream_iterator读取输入流,ostream_iterator向一个输出流写入数据。这些迭代器将它们的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。

istream_iterator操作
当创建一个流迭代器时,必需指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。因此,一个istream_iterator读写的对象类型必需定义了输入运算符。

流迭代器的读取操作实际上是在迭代器向前递增的时候,并非是在被引用的时候。而第一个读取的对象是在istream_iterator被构造的时候发生。默认的istream_iterator对象被称为尾后迭代器,当一个合法的迭代器到达容器的尾部的时候,它就会和尾后迭代器相等。

#include <iostream>
#include <sstream>
#include <iterator>
#include <string>
#include <vector> using namespace std; int main(){
string str("1 2 3 4 5");
istringstream sstrm(str); istream_iterator<int> iter(sstrm);//从istringstream读取int
istream_iterator<int> eof;//尾后迭代器
while(iter != eof){//当有数据可读时
cout << *iter << " ";
iter++;
}
cout << endl; return ;
}

结果:

1 2 3 4 5 

ostream_iterator操作

一个ostream_iterator使用<<来向流中写入数据。因此,一个ostream_iterator读写的对象类型必需定义了输出运算符。

当创建一个ostream_iterator时,构造器提供了第二个(可选的)参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是C风格字符串(即,一个字符串字面常量或者一个指向以空字符结尾的字符数组的指针)。必需将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator。

#include <iostream>
#include <sstream>
#include <iterator> using namespace std; int main()
{
ostringstream str;
//使用ostringstream构建一个ostream_iterator迭代器
//在每次写入字符后都会附加一个空格
ostream_iterator<string> iter(str," "); //写入字符
iter = "how";
iter = "are";
iter = "you"; cout << str.str() << endl;
}

结果:

how are you

由于istream_iterator使用>>读取流数据,ostream_iterator使用<<操作符向流写入数据,所以我们只需要为我们的定义上合适的<<和>>操作符,就可以使用istream_iterator和ostream_iterator流迭代器了。

person.h文件

#pragma once
#include <string>
#include <iostream>
class person{
//声明友好方法
friend std::istream& operator>>(std::istream&,person&);
friend std::ostream& operator<<(std::ostream&,const person&); private:
std::string name;
public:
person(){} std::string getName() const{
return this->name;
}
};
//istream_iterator会调用>>向对象写数据
std::istream& operator>>(std::istream& is,person& p){
is >> p.name;
return is;
};
//ostream_iterator会调用<<向流中写入数据,由于只读person的值,所以声明为const person&
std::ostream& operator<<(std::ostream& os,const person& p){
os << "person name : " << p.name;
return os;
};

personTest.cpp文件

#include "person.h"/*person*/
#include <iterator>/*istream_iterator,ostream_iterator*/
#include <sstream>/*istringstream*/
#include <vector>/*vector*/
#include <algorithm>/*for_each*/
#include <iostream>/*cout,endl*/ using namespace std; int main(int argc,char* argv[]){
istringstream issm("green blue"); //istream_iterator会调用person的>>从流中读数据
istream_iterator<person> ist(issm);
istream_iterator<person> eof; vector<person> vec;
while(ist != eof){
vec.push_back(*ist);
ist++;
} //ostream_iterator会调用person的<<向流中写数据
ostream_iterator<person> os(cout,"\n"); for_each(vec.begin(),vec.end(),[&](const person& p){
os = p;
}); cout << endl;
return ;
}

结果:

person name : green
preson name : blue

3.4 反向迭代器

反向迭代器就是在容器中从尾元素向首元素反向移动迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增反向迭代器(++it)会移动到前一个元素;递减反向迭代器(--it)会移动到下一个元素。

除了forward_list之外,其它容器都支持反向迭代器。我们可以通过调用rbegin,rend,crbegin和crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。与普通迭代器一样,反向迭代器也有const和非const的版本。

反向迭代器(reverse_iterator)在头文件<iterator>中,reverse_iterator实际是一个迭代器适配器,它会反转给它的容器。换句话说,当反转一个双向迭代器时,reverse_iterator会产生一个新的迭代器,该迭代器从序列的尾部向头部移动。

对于一个由迭代器i构成的反转迭代器r来说,这样的关系&*r = &*(i - 1)总是成立的
【C++】C++中的迭代器

通过这个图片,我们可以看出,反向迭代器并没有真正地反转序列中的元素,它只是从序列尾部向头部遍历而已。并且反向迭代器和源迭代器之间只错位了1个元素。

base函数会返回该反向迭代器对应的源迭代器,比如上面图片中

反向迭代器的rend对应源迭代器的begin
反向迭代器的1对应源迭代器中2
反向迭代器的2对应源迭代器中3
反向迭代器的3对应源迭代器中4
......
反向迭代器的14对应源迭代器中15
反向迭代器的15对应源迭代器中16
反向迭代器的rbegin对应源迭代器中end

有了base函数,我们就可以轻松在反向迭代器和源迭代器之间切换,下面实现了字符串从尾部查找特定字符,并且截取。

#include <iterator>/*reverse_iterator*/
#include <string>/*string*/
#include <algorithm>/*find*/
#include <iostream>/*cout,endl*/ using namespace std; int main(){
string line("first,middle,last");
reverse_iterator<string::iterator> rcomma = find(line.rbegin(),line.rend(),',');
//输出 tsal
cout << string(line.rbegin(),rcomma) << endl; //输出 last
cout << string(rcomma.base(),line.rbegin().base()) << endl;
return ;
}

第一个输出语句输出了tsal,这显然不是我们想要的,为了得到正确的答案需要得到它的源迭代器,然后再进行构造新的字符串。