除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器。这些迭代器包括以下几种。
插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。下表列出了这种迭代器支持的操作。
插入迭代器操作 |
it=t 在it指定的当前位置插入值t。假定c是it绑定的容器,依赖于插入迭代器的不同种类,此赋值分别调用c.push_back(t)、c.push_front(t)或c.insert(t,p),其中p为传递给inserter的迭 代器位置 *it,++it,it++ 这些操作虽然存在,但不会对it做任何事情。每个操作都返回it |
插入迭代器有三种类型,差异在于元素插入的位置:
注意:只有在容器支持push_front的情况下,我们才可以使用front_inserter。类似的,只有在容器支持push_back的情况下,我们才能使用back_inserter
理解插入迭代器的工作过程是很重要的:当调用inserter(c,iter)时,我们得到一个迭代器,接下来使用它时,会将元素插入到iter原来所指的位置之前的位置。即,如果it是由inserter生成的迭代器,则下面这样的赋值语句
*it=val;
其效果与下面代码一样
it=c.insert(it,val);//it指向新加入的元素
++it; //递增it使它指向原来的元素
front_inserter生成的迭代器的行为与inserter生成的迭代器完全不一样。当我们使用front_inserter时,元素总是插入到容器第一个元素之前,即使我们传递给inserter的位置原来指向第一个元素,只要我们在此元素之前插入一个新元素,此元素就不再是容器的首元素了:
list<int> lst={1,2,3,4};
list<int> lst2,lst3; //空list
//拷贝完成之后,lst2包含4 3 2 1
copy(lst.begin(),lst.end(),front_inserter(lst2));
//拷贝完成之后lst3包含1 2 3 4
copy(lst.begin(),lst.end(),inserter(lst3,lst.begin()));
当调用front_inserter(c)时,我们得到一个插入迭代器,接下来会调用push_front.当每个元素被插入到容器c中时,它变为c的新的首元素。因此,front_inserter生成的迭代器会将插入的元素序列的顺序颠倒过来,而inserter和back_inserter则不会。
虽然iostream类型不是容器,但标准库定义了用于这些IO类型对象的迭代器。istream_iterator读取输入流,ostream_iterator向一个输出流写数据。这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。
istream_iterator操作
当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。因此,istream_iterator要读取的类型必须定义了输入运算符。当创建一个istream_iterator时,我们可以将它绑定到一个流。当然,我们还可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器。
istream_iterator<int> int_it(cin); //从cin读取int
istream_iterator<int> int_eof; //尾后迭代器
ifstream in("afile");
istream_iterator<string> str_in(in); //从“afile读取字符串
下面是一个用istream_iterator从标准输入流读取数据,存入一个vector的例子:
istream_iterator<int> in_iter(cin); //从cin读取int
istream_iterator<int> eof; //istream尾后迭代器
while(in_iter!=eof)
//后置递增运算读取流,返回迭代器的旧值
//解引用迭代器,获得从流读取的前一个值
vec.push_back(*in_iter++);
此循环从cin读取int值,保存在vec中。在每个循环步中,循环体代码检查in_iter是否等于eof。eof被定义为空istream_iterator,从而可以当作尾后迭代器来使用。对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。
我们可以将程序重写为如下形式,这体现了istream_iterator更有用的地方:
istream_iterator<int> in_iter(cin),eof; //从cin读取int
vector<int> vec(in_iter,eof); //从迭代器范围构造vec
本例中我们使用了一对表示范围的迭代器来构造vec,这两个迭代器是istream_iterator,这意味着元素范围是通过从关联的流中读取数据获得的。这个构造函数从cin读取数据,直至遇到文件尾或者遇到一个不是int的数据为止。从流中读取的数据被用来构造vec。
istream_iterator操作 |
istream_iterator<T> in(is); in从输入流is读取类型为T的值 istream_iterator<T> end; 读取类型为T的值的istream_iterator迭代器,表所尾后位置 in1==in2 in1和in2必须读取相同类型。如果它们都是尾后迭代器,或绑定到相同的输入,则两个相等 in1!=in2 *in 返回从流中读取数据 in->mem 与(*in).mem的含义相同 ++in,in++ 使用元素类型所定义的>>运算符从输入流中读取下一个值。与以往一样,前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值 |
使用算法操作流迭代器
由于算法使用迭代器操作来处理数据,而流迭代器又至少支持某种迭代器操作,因此我们至少可以用某些算法来操作流迭代器。下面是一个例子,我们可以用一对istream_iterator来调用accumulate:
istream_iterator<int> in(cin),eof;
cout<<accumulatre(in,eof,0)<<endl;
此调用会计算出从标准输入读取的值的和。如果输入为:
1 3 7 9 9
输出为29
istream_iterator允许使用懒惰求值
当我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从中读取数据,直到我们使用迭代器时才真正读取。标准库中的实现所保证的是,在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成了。对于大多数程序来说,立即读取还是推迟读取并没有什么差别。但是,如果我们创建了一个istream_iterator,没有使用就销毁了,或者我们正在从两个不同的对象同步读同一个流,那么何时读取可能就很重要了。
ostream_iterator操作
我们可以对任何输出运算符(<<运算符)的类型定义ostream_iterator。当创建一个ostream_iterator时,我们可以提供(可选的)第二参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个C风格字符串(即,一个字符串字面值或者一个指向以空字符结尾的字符数组的指针)。必须将ostream_iterator绑定到一个指定的流。不允许空的或表示尾后位置的ostream_iterator。
ostream_iterator操作 |
ostream_iterator<T> out(os); out将类型为T的值写到输出流os中 ostream_iterator<T> out(os,d); out将类型为T的值写到输出流os中,每个值后面都输出一个d。d指向一个空字符串结尾的字符数组 out=val 用<<运算符将val写入到out所绑定的ostream中。val的类型必须与out可写的类型兼容 *out,++out,out++ 这些运算符是存在的,但不对out做任何事情。每个运算符都返回out |
我们可以使用ostream_iterator来输出值的序列:
ostream_iterator<int> out_iter(cout," ");
for(auto e:vec)
*out_iter++=e; //赋值语句实际上将元素写到cout
cout<<endl;
此程序将vec中的每个元素写到cout,每个元素加一个空格,每次向out_iter赋值时,写操作就会被提交。
值得注意的是,当我们向out_iter赋值时,可以忽略解引用和递增运算。即,循环可以重写成下面的样子:
for(auto e:vec)
out_iter=e;//赋值语句将元素写道cout
cout<<end;
运算符*和++实际上对ostream_iterator对象不做任何事情,因此忽略它们对我们的程序没有任何影响。但是,推荐第一种形式。在这种写法中,流迭代器的使用与其他迭代器的使用保存一致。如果想将此循环改为操作其他迭代器类型,修改起来非常容易。而且,对于读者来说,此循环的行为也更为清晰。
可以通过调用copy来打印vec中的元素,这比编写循环更为简单:
copy(vec.begin(),vec.end(),out_iter);
cout<<endl;
使用流迭代器处理类类型
我们可以为任何定义了输入运算符(>>)的类型创建istream_iterator对象。类似的,只要类型有输出运算符(<<),我们就可以为其定义ostream_iterator。由于Sales_item既有输入运算符也有输出运算符,因此可以使用IO迭代器。例如:
istream_iterator<Sales_item> item_iter(cin),eof; ostream_iterator<Sales_item> out_iter(cout,"\n"); Sales_item sum=*item_iter++; while(item_iter!=eof) { if(item_iter->isbn()==sum.isbn()) sum+=*item_iter++; else { out_iter=sum; sum=*item_iter++; } } out_iter=sum;
此程序使用item_iter从cin读取Sales_item交易记录,并将和写入cout,每个结果后面都跟一个换行符。定义了自己的迭代器后,我们就可以用item_iter读取第一条交易记录,用它的值来初始化sum.
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器(++it)会移动到前一个元素;递减一迭代器(--it)会移动到下一个元素。
除了forward_list之外,其他容器都支持反向迭代器。我们可以通过调用rbegin、rcend、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!=vec.crend();++r_iter)
cout<<*r_iter<<endl; //打印9,8,7,6,5,4,3,2,1,0
虽然颠倒递增和递减运算符的含义可能令人混淆,但这样做是我们可以用算法透明地向前或向后处理容器。例如,可以通过向sort传递一对反向迭代器来将vector整理为递减序:
sort(vec.begin(),vec.end());
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()
但我们试图打印找到的单词时,看起来下面的代码是显然的方法
//错误:将逆序输出单词的字符
cout<<string(line.crbegin(),rcomma)<<endl;
但它会生成错误的输出结果。例如,如果我们的输入是
FIRST,MIDOLE,LAST
则这条语句会打印TSAL!
问题所在:我们使用的是反向迭代器,会反向出来string。因此,上述输出语句从crbegin开始反向打印line中内容。而我们希望按正常顺序打印从rcomma开始到line末尾间的字符。但是,我们不能直接使用rcomma。因为它是一个反向迭代器,意味着它会反向朝着string的开始位置移动。需要做的是,将rcomma转换回一个普通迭代器,能在line中正向移动。我们通过调用reverse_iterator的base成员函数来完成这一转换,此成员函数会返回其对应的普通迭代器:
//正确:得到一个正向迭代器,从逗号开始读取字符直到line末尾
cout<<string(rcomma.base(),line.cend())<<endl;
rcomma和rcomma.base()指向了不同的元素,line.crbegin()和line.cend()也是如此。这些不同保证了元素范围无论是正向处理还是反向出来都是相同的。
从技术上讲,普通迭代器与反向迭代器的关系反映了左闭合区间的特征。关键点在于[line.crbegin(),rcomma)和[rcomma.base(),line.cend())指向line中相同的元素范围。