再探迭代器(插入迭代器、流迭代器、反向迭代器、移动迭代器)

ⅰ亾dé卋堺 提交于 2019-11-28 20:02:45

除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器。这些迭代器包括以下几种。

  • 插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素
  • 流迭代器:这些迭代器被绑定到输入或输出上,可用来遍历所有关联的IO流
  • 反向迭代器:这些迭代器向后而不是向前移动。除了forward_list之外的标准库容器都有反向迭代器
  • 移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们。

 

1 插入迭代器

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

插入迭代器操作

 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

插入迭代器有三种类型,差异在于元素插入的位置:

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

注意:只有在容器支持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则不会。

 

2 iostream迭代器

虽然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.

 

3 反向迭代器

反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器(++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中相同的元素范围。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!