专栏简介:本专栏主要面向C++初学者,解释C++的一些基本概念和基础语言特性,涉及C++标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级程序设计技术。希望对读者有帮助!
目录
- 10.4再探迭代器
- 插人迭代器
- iostream迭代器
- 使用算法操作流迭代器
- ostream_iterator操作
- 使用流迭代器处理类类型
- 反向迭代器
- 反向迭代器需要递减运算符
10.4再探迭代器
除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器。这些迭代器包括以下几种。
- 插入迭代器(insert iterator):这些迭代器被绑定到一个容器上,可用来向容器插入元素。
- 流迭代器(stream iterator):这些迭代器被绑定到输入或输出流上,可用来遗历所关联的IO流。
- 反向迭代器(reverse iterator):这些迭代器向后而不是向前移动。除了forward_1ist之外的标准库容器都有反向迭代器。
- 移动迭代器(move iterator):这些专用的迭代器不是拷贝其中的元素,而是移动它们。
插人迭代器
插入器是一种迭代器适配器,它接受一个容器,生成一个追代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。表10.2列出了这种迭代器支持的操作。
表10.2:插入迭代器操作
it=t | 在tt指定的当前位置插入值t。假定c是it绑定的容器,依赖于揣入迭代器的不同种类,此赋值会分别调用c.push_back(t)、c.push_front(t)或c.insert(tp),其中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)时,我们得到一个迭代器,接下来使用它时,会将元素插入到itez原籼所指向的元素之前的位置。即,如果it是由inserter生成的迭代器,则下面这样的赋值语句
*it = val;
其效果与下面代码一样
it=c.insert(it,val);//it指向新加入的元素
++it;//递增it使它指向原来的元素
front_inserter生成的迭代器的行为与insertez生成的迭代器完全不一样。当我们使用front_inserter时,元素总是插入到容器第一个元素之前。即使我们传递给inserter的位置原来指向第一个元素,只要我们在此元素之前插入一个新元素,此元素就不再是容器的首元素了:
list<int>lst={1,2,3,4};
list<int>lst2,lst3;//空list
//拷贝完成之后,lst2包含4 3 2 1
copy(lst.cbegin(),lst.cend(),front_tnserter(lst2));
//拷贝完成之后,lst3包含1 2 3 4
copy(lst.cbegin(),lst.cend(),inserter(lst3,lst3.begin()));
当调用front_inserter©时,我们得到一个插入迭代器,接下来会调用push_front。当每个元素被插入到容器c中时,它变为c的新的首元素。因此,front_inserter生成的迭代器会将插入的元素序列的顺序颠倒过来,而inserter和back_inserter则不会。
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_it(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错误,迭代器的值就与尾后迭代器相等。
此程序最困难的部分是传递给push_back的参数,其中用到了解引用运算符和后置递增运算符。该表达式的计算过程与我们之前写过的其他结合解引用和后置递增运算的表达式一样。后置递增运算会从流中读取下一个值,向前推进,但返回的是迭代器的旧值。迭代器的旧值包含了从流中读取的前一个值,对迭代器进行解引用就能获得此值。
我们可以将程序重写为如下形式,这体现了istream_iterator更有用的地方:
istream_iterator<int>in_tter(cin),eof;//从cin读取int
vector<int>vec(in_iter,eof);//从迭代器范围构造vec
本例中我们用一对表示元素范围的迭代器来构造vec。这两个迭代器是istream_iterator,这意味着元素范围是通过从关联的流中读取数据获得的。这个构造函数从cin中读取数据,直至遇到文件尾或者遇到一个不是int的数据为止。从流中读取的数据被用来构造vec。
表10.3:istream_iterator操作
istream_iteratorin(is); | in从输入流is读取类型为0的值 |
istream_iteratorend; | 读取类型为的值的istream_itterator迭代器,表示尾后位置 |
in1=in2 | in1和in2必须读取相同类型。如果它们都是尾后迭代器,或绑定到相同 |
in1!=in2 | 的输入,则两者相等 |
*in | 返回从流中读取的值 |
n->mem | 与(*in).mem的含义相同 |
++inn++使用元素类型所定义的>>运算符从输入流中读取下一个值。与以往一样,前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值 |
使用算法操作流迭代器
由于算法使用迭代器操作木处理数据,而流迭代器又至少支持树些选代器操作,因此我们至少可以用桅些算法来操作流迭代器。下面是一个例子,我们可以用一对istream_iterator来调用accumulate:
istream_iterator<int>in(cin),eof;
cout<<accumulate(in,eof,0)<<endl;
此调用会计算出从标准输入读取的值的和。如果输入为:
23 109 45 89 6 34 12 90 34 23 56 23 8 89 23
则输出为664。
istream_iterator允许使用懒惰求值
当我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。标准库中的实现所保证的是,在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成了。对于大多数程序来说,立即读取还是推迟读取没什么差别。但是,如果我们创建了一个istream_iterator,没有使用就销毁了,或者我们正在从两个不同的对象同步读取同一个流,那么何时读取可能就很重要了。
ostream_iterator操作
我们可以对任何具有输出运算符(<<运算符的类型定义ostream_iterator。当创建一个ostream_iterator时,我们可以提供(可选的)第二参数,它是一个字符流,在输出每个元素后都会打印此字符串。此字符串必须是一个C风格字符串(即,一个字符串字面常量或者一个指向以空字符结尾的字符数组的指针)。必须将ostream_iterator绑定到一个指定的流,不允许宇的或表示尾后位置的ostream_iterator。
表10.4:ostream_iterator操作
ostream_iterator out(os) | out将类型T的值写到输出流os中 |
ostream_iterator_out(os,d); | out将类型为7的值写到输出流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<<endl;
运算符*和++实际上对ostream_iterator对象不做任何事情,因此忽略它们对我们的程序没有任何影响。但是,推荐第一种形式。在这种写法中,流迭代器的使用与其他迭代器的使用保持一致。如果想将此循环改为操作其他迭代器类型,修改起来非常容易。而且,对于读者来说,此循环的行为也更为清晰。可以通过调用copy来打印vec中的元素,这比编写循环更为简单:
copy(vec.begin(),vec.end(),out_iter);
cout<<endl;
使用流迭代器处理类类型
我们可以为任何定义了输入运算符(>>的类型创建istream_iterator对象。类似的,只要类型有输出运算符(C<<),我们就可以为其定义ostream_iterator。由于Sales_item既有输入运算符也有输出运算符:
istream_iterator<Sales_item>item_iter(cin),eof;
ostream_iterator<Sales_item>out_iter(cout,"\n");
//将第一笔交易记录存在sum中,并读取下一条记录
Sales_item sum =*item_iter++;
while(item_iter!=eof){//如果当前交易记录(存在ttem_iter中)有着相同的ISBN号if(ttem_iter->isbn()==sum.isbn())sum+=*item_iter++;//将其加到sum上并读取下一条记录else{out_iter= sum;//输出sum当前值sum = *item_iter++;//读取下一条记录}
}
out_iter = sum;//记得打印最后一组记录的和
此程序使用item_iter从cin读取Sales_item交易记录,并将和写入cout,每个结果后面都跟一个换行符。定义了自己的迭代器后,我们就可以用item_iter读取第一条交易记录,用它的值来初始化sum:
//将第一条交易记录保存在sum中,并读取下一条记录
Sales_item sum = *item_iter++;
此处,我们对item_iter执行后置递增操作,对结果进行解引用操作。这个表达式读取下一条交易记录,并用之前保存在item_iter中的值来初始化sum。
while循环会反复执行,直至在cin上遇到文件尾为止。在while循环体中,我们检查sum与刚刚读入的记录是否对应同一本书。如果两者的ISBN不同,我们将sum赋予out_iter,这将会打印sum的当前值,并接着打印一个换行符。在打印了前一本书的交易金额之和后,我们将最近读入的交易记录的副本赋予sum,并递增迭代器,这将读取下一条交易记录。循环会这样持续下去,直至遇到错误或文件尾。在退出之前,记住要打印输入中最后一本书的交易金额之和。
反向迭代器
反向追代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增〔以及递减)操作的含义会颠倒过来。递增一个反向迭代器(++it会移动到前一个元素;递减一个迭代器(一it会移动到下一个元素。除了forward_1ist之外,其他容器都支持反向迭代器。我们可以通过调用rbegin、rend、crbegin和crend成员函数来获得反向追代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。与普通迭代器一样,反向迭代器也有const和非const版本。
下面的循环是一个使用反向迭代器的例子,它按逆序打印vec中的元素:
vector<int>vec={0,1,2,3,415,6,7,8,9};
//从尾元素到首元素的反向选代器
for(auto x_iter=vec.crbegin();//将_iter绑定到尾元素x_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 comma=find(line.crbegin(),line.czend(),',');
由于我们将crbegin()和crend()传递给find,find将从1ine的最后一个字符开始向前搜索。当find完成后,如果1ine中有逗号,则rcomma指向最后一个逗号一一即,它指向反向搜索中找到的第一个逗号。如果1ine中没有逗号,则rcomma指向
line.crend()。
当我们试图打印找到的单词时,最有意思的部分就来了。看起来下面的代码是显然的方法
//错误:将送序输出单词的字符
cout<<string(line.crbegin(),rcomma)<<endl;
但它会生成错误的输出结果。例如,如果我们的输入是
FIRST,MIDDLE,LAST
则这条语句会打印TSAL!
从技术上讲,普通迭代器与反向迭代器的关系反映了左闭合区间的特性。关键点在于[line.crbegin(),rcomma)和[rcomma.base(),line.cend())指向line中相同的元素范围。为了实现这一点,rcomma和rcomma.base()必须生成相邻位置而不是相同位置,crbegin()和cend()也是如此。
反向迭代器的目的是表示元素范围,而这些范围是部队称的,这导致一个重要的结果:当我们从一个普通速代器初始化一个反向追代器,或是给一个反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同的元素。