【C++基础】第五十九课:[泛型算法]再探迭代器

插入迭代器,back_inserter,front_inserter,inserter,流迭代器,istream_iterator,ostream_iterator,反向迭代器,rbegin,rend,crbegin,crend,reverse_iterator,base

Posted by x-jeff on December 24, 2022

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.再探迭代器

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

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

2.插入迭代器

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

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

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

只有在容器支持push_front的情况下,我们才可以使用front_inserter。类似的,只有在容器支持push_back的情况下,我们才能使用back_inserter。

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

1
*it = val;

其效果与下面代码一样:

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

接下来看一个例子:

1
2
3
4
5
6
7
8
9
list<int> lst = {1,2,3,4};
list<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()));
//个人理解:
//lst3本来是空的,插进来第一个元素1之后,插入迭代器其实最终指向的是lst3.end()。
//所以如果再插进来第二个元素2,会被插在lst3.end()之前,其实也就是1之后。后面依此类推。

copy函数的使用:拷贝算法

3.iostream迭代器

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

3.1.istream_iterator操作

当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。因此,istream_iterator要读取的类型必须定义了输入运算符。当创建一个istream_iterator时,我们可以将它绑定到一个流。当然,我们还可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器。

下面是一个用istream_iterator从标准输入读取数据,存入一个vector的例子:

1
2
3
4
5
6
istream_iterator<int> in_iter(cin);//从cin读取int
istream_iterator<int> eof;//istream尾后迭代器
while(in_iter != eof)//当有数据可供读取时
	//后置递增运算读取流,返回迭代器的旧值
	//解引用迭代器,获得从流读取的前一个值
	vec.push_back(*in_iter++);

eof被定义为空的istream_iterator,从而可以当作尾后迭代器来使用。对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。

我们可以将程序重写为如下形式,这体现了istream_iterator更有用的地方:

1
2
istream_iterator<int> in_iter(cin), eof;//从cin读取int
vector<int> vec(in_iter, eof);//从迭代器范围构造vec

3.2.使用算法操作流迭代器

由于算法使用迭代器操作来处理数据,而流迭代器又至少支持某些迭代器操作,因此我们至少可以用某些算法来操作流迭代器。比如:

1
2
istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;

此调用会计算出从标准输入读取的值的和。如果输入为:

1
23 109 45 89 6 34 12 90 34 23 56 23 8 89 23

则输出为664。

3.3.istream_iterator允许使用懒惰求值

当我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。标准库中的实现所保证的是,在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成了。对于大多数程序来说,立即读取还是推迟读取没什么差别。但是,如果我们创建了一个istream_iterator,没有使用就销毁了,或者我们正在从两个不同的对象同步读取同一个流,那么何时读取可能就很重要了。

3.4.ostream_iterator操作

我们可以对任何具有输出运算符(<<运算符)的类型定义ostream_iterator。当创建一个ostream_iterator时,我们可以提供(可选的)第二参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个C风格字符串(即,一个字符串字面常量或者一个指向以空字符结尾的字符数组的指针)。必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator。

我们可以用ostream_iterator来输出值的序列:

1
2
3
4
5
6
7
8
vector<int> vec = {4,2,6,9};
ostream_iterator<int> out_iter(cout, " ");
for(auto e : vec)
    *out_iter++ = e;//赋值语句实际上将元素写到cout
    //等价于*out_iter = e;
    //等价于*++out_iter = e;
cout << endl;
//输出为:4 2 6 9 

值得注意的是,当我们向out_iter赋值时,可以忽略解引用和递增运算。即,循环可以重写成下面的样子:

1
2
3
for(auto e : vec)
    out_iter = e;//赋值语句将元素写到cout
cout << endl;

运算符*++实际上对ostream_iterator对象不做任何事情,因此忽略它们对我们的程序没有任何影响。

可以通过调用copy来打印vec中的元素,这比编写循环更为简单:

1
2
copy(vec.begin(), vec.end(), out_iter);
cout << endl;

3.5.使用流迭代器处理类类型

我们可以为任何定义了输入运算符(>>)的类型创建istream_iterator对象。类似的,只要类型有输出运算符(<<),我们就可以为其定义ostream_iterator。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
{
	//如果当前交易记录(存在item_iter中)有着相同的ISBN号
	if (item_iter->isbn() == sum.isbn())
		sum += *item_iter++;//将其加到sum上并读取下一条记录
	else
	{
		out_iter = sum;//输出sum当前值
		sum = *item_iter++;//读取下一条记录
	}
}
out_iter = sum;//记得打印最后一组记录的和

4.反向迭代器

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

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

图10.1显示了一个名为vec的vector上的4种迭代器:

下面的循环是一个使用反向迭代器的例子,它按逆序打印vec中的元素:

1
2
3
4
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,...,0

再例如,可以通过向sort传递一对反向迭代器来将vector整理为递减序:

1
2
3
sort(vec.begin(), vec.end());//按“正常序”排序vec
//按逆序排序:将最小元素放在vec的末尾
sort(vec.rbegin(), vec.rend());

4.1.反向迭代器需要递减运算符

我们只能从既支持++也支持--的迭代器来定义反向迭代器。毕竟反向迭代器的目的是在序列中反向移动。除了forward_list之外,标准容器上的其他迭代器都既支持递增运算又支持递减运算。但是,流迭代器不支持递减运算,因为不可能在一个流中反向移动。因此,不可能从一个forward_list或一个流迭代器创建反向迭代器。

4.2.反向迭代器和其他迭代器间的关系

假定有一个名为line的string,保存着一个逗号分隔的单词列表,我们希望打印line中的第一个单词。使用find可以很容易地完成这一任务:

1
2
3
//在一个逗号分隔的列表中查找第一个元素
auto comma = find(line.cbegin(), line.cend(), ',');
cout << string(line.cbegin(), comma) << endl;

如果希望打印最后一个单词,可以改用反向迭代器:

1
2
//在一个逗号分隔的列表中查找最后一个元素
auto rcomma = find(line.crbegin(), line.crend(), ',');

当我们试图打印找到的单词时,最有意思的部分就来了。看起来下面的代码是显然的方法:

1
2
//错误:将逆序输出单词的字符
cout << string(line.crbegin(), rcomma) << endl;

但它会生成错误的输出结果。例如,如果我们的输入是:

1
FIRST,MIDDLE,LAST

则这条语句会打印TSAL!

图10.2说明了问题所在:我们使用的是反向迭代器,会反向处理string。我们需要做的是,将rcomma转换回一个普通迭代器,能在line中正向移动。我们通过调用reverse_iterator的base成员函数来完成这一转换,此成员函数会返回其对应的普通迭代器:

1
2
//正确:得到一个正向迭代器,从逗号开始读取字符直到line末尾
cout << string(rcomma.base(), line.cend()) << endl;

给定和之前一样的输入,这条语句会如我们的预期打印出LAST。

图10.2中的对象显示了普通迭代器与反向迭代器之间的关系。例如,rcomma和rcomma.base()指向不同的元素,line.crbegin和line.cend()也是如此。这些不同保证了元素范围无论是正向处理还是反向处理都是相同的。