迭代器是一个所谓的smart pointers。具有遍历复杂数据结构的能力,其下层运行机制取决于所遍历的数据结构。因此,每一种容器都必须提供自己的迭代器。事实上,每一种容器都将迭代器以嵌套的方式定义与内部,因此各种迭代器的接口相同,型别却不同。这直接导出了泛型程序设计的概念:所有操作行为都使用相同接口,虽然它们的型别不同。 迭代器还有一个非常纯粹的理念:任何东西,只要其行为类似迭代器,它就是一个迭代器。 这句话赋予了迭代器更强大的抽象能力,也是后面理解迭代器适配器的关键。
迭代器分类:
读取(Input)迭代器 写入(Output)迭代器 (前向)Forward迭代器 (双向)Bidirectional 迭代器 (随机存取)Random access迭代器Input迭代器: Input迭代器只能一次一个向前读取元素,按此顺序一个个传回元素值。 注意,Input迭代器只能读取元素一次,如果你复制Input迭代器,并使原Input迭代器和新副本都向前读取,可能会遍历到不同的值。典型的是从标准输入读取数据的迭代器。 Input迭代器所支持的操作只有:自增,解引用, 判断两个迭代器是否相等
Output迭代器: 与Input迭代器相对应,将元素值一个个写入,即你只能一个个元素地赋新值,并且不能对同一序列进行两次遍历。常用于向标准输出。 所支持的操作:自增,解应用(只能作左值) 注意:Output迭代器无需比较操作,你无法检查Output迭代器是否有效,或写入操作是否成功,你唯一能做的就是写入写入再写入。
Forword迭代器: Forword迭代器是Input和Output迭代器的结合,具备Input迭代器的全部功能和Output迭代器的部分功能,之所以说Output的部分功能是因为对Output适配器的写入不需要判断迭代器的有效性(Output迭代器根本不支持比较),而Forword迭代器必须要事先判断,否则可能引发为未定义行为。
Bidirectional 迭代器 Bidirectional迭代器在Forword迭代器的基础上增加了回头遍历的=功能,即支持递减
Random Access迭代器: 随机存取迭代器用于支持随机存取(如 vector deque string等)的容器。它提供指针的算术运算,如加减常量,迭代器之间相减,迭代器之间比较等。
一个例子:
#include "stdafx.h"
#include<iostream>
#include<vector>
#include<list>
#include<deque>
#include<algorithm>
using namespace std;
int _tmain(int argc, _TCHAR* argv [])
{
list<int > iList;
vector<int > iVec;
for (int i=0 ; i< 9; i ++)
{
iList .push_back(i);
}
iVec.resize (iList. size());
copy(iList .begin(), iList.end (), iVec. begin());
deque<int > iDeq( iList.size ());
copy(iList .begin(), iList.end (), iDeq. begin());
return 0;
}
这个例子乍一看挺简单的,但是对于初学者却经常忘记在调用copy之前先对目标容器分配足够的空间,以至于导致为未定义行为。这里说的分配空间可以通过对已经构造的容器调用resize或在容器构造函数中传入大小来完成。这两种方式都会以默认值初始化元素,如果要指定初始值,可以在第二参数附加指定默认值。 如果将上面的代码改为: iList::iterator iterBegin = find(iList.begin(), iList.end(), 3); iList::iterator iterEnd = find(iterBegin, iList.end(), 7); 现在我要将iterBegin 和 iterEnd之间的元素复制给iVec,又该如何做呢?此时由于iList的迭代器不是随时存取迭代器,因此无法通过iterEnd-iterBegin来算出元素个数,当然我们也不能取巧的通过iList里面的内容来手动计算。此时就需要用到STL中另一种更只能的组件 ----- 迭代器适配器
迭代器适配器:
先看看如何通过迭代器适配器来解决以上问题的吧: //头文件略
int _tmain(int argc, _TCHAR* argv [])
{
list<int > iList;
vector<int > iVec;
for (int i=0 ; i< 9; i ++)
{
iList .push_back(i);
}
list<int >::iterator iterBegin = find(iList .begin(), iList.end (), 3);
list<int >::iterator iterEnd = find(iterBegin , iList. end(), 7);
copy(iList .begin(), iList.end (), back_inserter(iVec));
deque<int > iDeq;
copy(iList .begin(), iList.end (), back_inserter(iDeq));
return 0;
}
可以看到本例和上面不一样的地方:
1.无需对目标容器分配容量
2.copy的第三个参数变为了back_inserter(iVec)
back_inserter就是一种迭代器适配器,确切一些,它是安插型迭代器(Insert Iterators或称为Inserters)的一种。它的作用是在指定容器后面插入元素。它与普通迭代器不同的地方就在于它通过插入方式而非覆写方式来运作。前面曾经提到迭代器具有极高的抽象能力:任何东西,只要其行为类似迭代器,它就是一个迭代器。这句话放在这里可能比较难理解,我的理解是,任何能提供其接口(赋值,解应用,自增等)并能够取代一般迭代器完成更复杂功能的类别都可以作为迭代器,尽管它可能与一般的迭代器(更像是一个指针)有很大不同(它们可能内置一些处理和算法)。STL提供了数个预先定义的特殊迭代器,这就是迭代器适配器。
STL提供了三种迭代器适配器: 安插型迭代器(Insert iterators或Inserters) 流迭代器(Stream iterators) 逆向迭代器(Reverse iterators)
Insert iterator迭代器:
Insert iterator迭代器可以使算法以安插(Insert)方式而非覆写(Overwrite)方式运作。使用它,可以解决目标空间分配不足问题,它会促使目标容器空间的大小按需成长。 Insert Iterator迭代器对如下接口进行了新定义:- 赋值(assign):如果你对某个元素设值,会引发"对其所属容器的安插(insert)操作",至于插入位置是在最前或最后,或是在某个特定位置上,视三种insert iterators而定。
- 单步前进(step forward):不会造成任何动静(no-op)
namespace std
{
template<class Inputiterator, class Outputiterator >
Outputiterator copy (Inputiterator from_pos,
Inputiterator from_end ,
Outputiterator to_pos )
{
while ( from_pos != from_end )
{
*to_pos = *from_pos ;
from_pos ++;
to_pos ++;
}
return to_pos;
}
}
其中*关键就在to_pos = *from_pos,Insert迭代器可以把上述操作转换为安插操作。这里面分为两个操作:
首先operator*传回迭代器当前位置
由operator=执行赋值
对于Insert迭代器来说:
operator*被定义为一个no-op 即无实际操作,只是简单传回*this,所以对Insert迭代器来说,*pos和pos等价
赋值操作被转换为安插操作,即由Insert迭代器调用容器的push_back(),push_front(),或者insert()成员函数。
另外,在赋值之后的to_pos++ 也被insert迭代器定义为无操作。Insert迭代器的很多特性都和Output型迭代器相似---只管写入(在重载"="运算符中实现)
Stream Iterators流迭代器:
不同于普通迭代器和安插迭代器,流迭代器是用于读写stream的迭代器,它提供了必要的抽象性,使得来自键盘或文件的输入像是个群集(collection),你能对它进行读取,同样,也能够将计算结果写入(输出)到文件或者屏幕上。 下面的代码展示了迭代器适配器的巨大威力:#include "stdafx.h"
#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;
int _tmain(int argc, _TCHAR* argv [])
{
vector<string > vecStr;
//从标准输入读取单词到vecStr
copy(istream_iterator <string>( cin),
istream_iterator <string>(),
back_inserter (vecStr));
//将单词排序
sort(vecStr .begin(), vecStr.end ());
//将排序后的单词输出到cout,并去除相邻重复单词
unique_copy (vecStr. begin(), vecStr .end(),
ostream_iterator <string>( cout, "\n"));
return 0;
}
上面的代码充分显示了算法,流迭代器和安插迭代器配合的巨大威力,在不到10行的核心代码里,实现了从标准输入读入单词,对单词排序,去除重复单词并输出到屏幕。
上面用到两种流迭代器,istream_iterator和ostream_iterator:
istream_iterator<string>(cin):
该迭代器通过Input 迭代器接口从标准输入cin读取数据,其中模板参数string代表该流迭代器从标准输入读入型别是字符串,型别是通过>>运算符被读进来的,因此每当算法企图读取下一个元素时istream_iterator就会将读取操作转换为cin>>string
注意:istream迭代器的构造函数会将stream打开,读取第一个元素,因此在使用istream迭代器之前,被过早定义它。否则一旦operator*被调用,就无法返回第一个元素了。(参见Input iterator特性)
istream_iterator<string>(): 调用istream_iterator<string>()的默认构造函数,产生一个代表"流结束符号"的迭代器,它代表的意义是:你不能再从中读取任何东西。 只要不断读取的第一个参数不等于第二个参数,算法就将继续执行。而这里的第二个参数"流结束符"的作用就是代表文件结束符或者ctrl+c,这个算法从cin读取所有string,直到遇到文件结束符(控制台下ctrl+c)为止,将读取到的所有元素安插到vector vecStr中。
ostream_iterator<string>(cout, "\n"): 会产生一个output stream iterator,实现output iterator接口,通过operator<<向cout写入strings。第二个参数是可选的,作为cout输出元素之间的分隔符。本例输出换行。 ostream_iterator和前面所说Insert迭代器概念一致(包括对自增无操作,解引用返回iter),唯一的区别在于它将赋值操作转换为operator<<。
另一个示例:
#include "stdafx.h"
#include<iostream>
#include<algorithm>
#include<string>
using namespace std;
int _tmain(int argc, _TCHAR* argv [])
{
istream_iterator <string> cinPos(cin );
ostream_iterator <string> coutPos(cout , " " );
istream_iterator <string> endofInput = istream_iterator <string>();
while (cinPos != endofInput)
{
//将当前单词赋给ostream迭代器
*coutPos++ = *cinPos++;
//istream流迭代器向前2个元素 即跳过两个单词
//advance(cinPos, 2);
}
cout<<endl ;
}
上面的代码将从输入的每一个单词(因为cout默认将空格作为字符串分隔)都依次输出。通过前面对流迭代器的讲解可以知道,*coutPos++ = *cinPos++换成: coutPos++ = *cinPos++; coutPos = *cinPos++; *coutPos = *cinPos++; 都是一样的,而对于cinPos却不行。 有两个令人费解的地方: 1.程序总是不能输出最后一个单词,最后一次单词将在下次输出时第一个输出,即最后一个单词并缓冲。将上面代码改为: * coutPos ++ = * cinPos; cinPos++; 即可以每次输出所有输入,这应该和cin和cout的联合刷新机制(tie())有关系(见C++ primer):当使用*cinPos++时,在对cinPos执行operator * 后,就会引起自增,此时由于单词已经读取完,因此又会等待用户输入,由于cin和cout的tie()机制(每一次输入之前都将刷新输出),该操作也将刷新输出缓冲,因此看到的输出总比输入少一个。 2.在控制台输入一系列单词,按Ctrl+Z 即输入EOF。并不能让程序终止,程序会输出当前结果,并且还可以继续输入。(这个问题本人也还在理解,按照《C++标准程序库》的说法,endofInput迭代器已经代表了string流的结束EOF,而我也输入了EOF,但是while总是为真)
Reverse Iterators(逆向迭代器):
逆向迭代器用逆向的方式对容器进行所有操作,它将递增运算转换成递减。通过它完成对容器的倒序操作。所有容器都可以通过rbegin()和rend()产生reverse iterators。
#include "stdafx.h"
#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;
int _tmain(int argc, _TCHAR* argv [])
{
vector<int > iVec;
for (int i=1 ; i<= 9; i ++)
{
iVec .push_back(i);
}
vector<int >::iterator pos;
pos = find (iVec. begin(), iVec .end(), 5 );
cout<<"pos : " <<*pos<<endl ;
vector<int >::reverse_iterator rpos(pos);
cout<<"rpos : " <<*rpos<< endl;
}
这个输出可能会让你大吃一惊,将一个双向迭代器转换成逆向迭代器竟然改变了迭代器所指元素的值。这也是逆向迭代器最值得注意的地方。 STL的逆向迭代器之所以这样实现,是为了保证区域的半开性:为了能够指定容器内的所有元素,我们必须指定最后一个元素的下一个位置。而对于Reserve而言,这个位置位于第一个位置之前,而这个位置可能并不存在,容器并不保证这一点,也就是说,我们可能无法获取rend()。 也就是说,元素值(实际位置)和迭代器值(逻辑位置)之间必须有所调整,要么保持迭代器值,调整元素值位置,或者保持元素值位置,调整迭代器值。根据前面的例子可以看出,STL采用方案一:保持逻辑位置。直接将原来的第一个位置begin()作为rend() 而将原来的end()作为rbegin()。这样使得rend指向的位置存在元素,而rbegin()指向的位置却不存在元素,因此还需要将元素依次"右移"一位,这样就能保证rend()是"最后一个元素的下一个位置"并且rend()存在。 这样做还有另一个好处就是区间变换: #include<algorithm>
#include<string>
using namespace std;
void print( int elem)
{
cout<<elem <<" ";
}
int _tmain(int argc, _TCHAR* argv [])
{
deque<int > iDeq;
for (int i=1 ; i<= 9; i ++)
{
iDeq .push_back(i);
}
//定位区间[posStart, posEnd)
deque<int >::iterator posStart;
posStart = find (iDeq. begin(), iDeq .end(), 2 );
deque<int >::iterator posEnd;
posEnd = find (posStart, iDeq.end (), 7 );
//打印出2和7之间的所有元素
for_each(posStart , posEnd, print);
cout<<endl ;
//转换为逆向迭代器[rposEnd, rposStart)
deque<int >::reverse_iterator rposStart(posStart);
deque<int >::reverse_iterator rPosEnd(posEnd);
//打印区间内元素
for_each(rPosEnd , rposStart, print);
cout<<endl ;
}
这样逆向迭代器形成的区间无需转换。通过逆向迭代器的base()函数可以将逆向迭代器转换成正常迭代器。