Part II: The C++ Library
Chapter 10. Generic Algorithms
10.3 再探迭代器
除了为每个容器定义的迭代器外,库在 iterator 头文件中还定义了其他几种迭代器:
- 插入迭代器 (insert iterator):与一个容器绑定,可用来向容器插入元素。
- 流迭代器 (stream iterator):与输入或输出流绑定,可用来遍历所相关的 IO 流。
- 反向迭代器 (reverse iterator):这些迭代器反向移动,从尾元素向首元素遍历容器。除了 forward_list 之外的库容器都有反向迭代器。
- 移动迭代器 (move iterator):这些专用的迭代器移动其中的元素,而不是复制它。(第13章)
插入迭代器
插入器是一种迭代适配器,接受一个容器,生成一个迭代器,实现增加元素到特定容器这个功能。
当通过插入迭代器赋值时,该迭代器调用容器操作,在给定容器的指定位置增加元素。
表10.2 插入迭代器操作
操作 | 说明 |
---|---|
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。
如果 it 是由 inserter 生成的迭代器,
*it = va1;
上面的赋值效果与下面一样,
it = c.insert(it, val); // it points to the newly added element
++it; // increment it so that it denotes the same element as before
当使用 front_inserter 时,元素总是插入到容器第一个元素的前面。
list<int> lst = {1,2,3,4};
list<int> lst2, lst3; // empty lists
// after copy completes, lst2 contains 4 3 2 1
copy(lst.cbegin(), lst.cend(), front_inserter(lst2));
// after copy completes, 1st3 contains 1 2 3 4
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));
iostream 迭代器
istream_iterator
读取输入流。ostream_iterator
写入输出流。这些迭代器将它们对应的流当作特定类型元素的序列。
表10.3 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++ | 使用元素类型的 >> 操作符从输入流中读取下一个值。前置版本返回递增后迭代器的引用,后置版本返回旧值。 |
表10.4 ostream_iterator 操作
操作 | 说明 |
---|---|
ostream_iterator<T> out(os); | out 将类型 T 的值写入输出流 os。 |
ostream_iterator<T> out(os,d); | out 将类型 T 的值后加上 d,写入输出流 os。d 指向一个空字符结尾的字符串数组。 |
out = val | 使用 << 操作符将 val 写入 out 绑定的 ostream。val 的类型必须与 out 可写的类型兼容。 |
*out, ++out, out++ | 这些操作存在,但不对 out 作任何事。每个操作都返回 out。 |
istream_iterator 上的操作
istream_iterator 读取的类型必须定义了输入运算符 >>。
istream_iterator<int> int_it(cin); // reads ints from cin
istream_iterator<int> int_eof; // end iterator value
ifstream in("afile");
istream_iterator<string> str_it(in); // reads strings from "afile"
下面的程序是使用 istream_iterator 从标准输入读取数据,存入 vector。
istream_iterator<int> in_iter(cin); // read ints from cin
istream_iterator<int> eof; // istream ''end'' iterator
while (in_iter != eof) // while there's valid input to read
// postfix increment reads the stream and returns the old value of the iterator
// we dereference that iterator to get the previous value read from the stream
vec.push_back(*in_iter++);
可以将上面的程序重写成如下形式:
istream_iterator<int> in_iter(cin), eof; // read ints from cin
vector<int> vec(in_iter, eof); // construct vec from an iterator range
在算法中使用流迭代器
istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl; // generate the sum of values read from the standard input
允许 istream_iterator 使用惰性求值 (Lazy Evaluation)
当将 istream_iterator 绑定到流上时,不能保证会立即读取流。
允许延迟读取流,直到使用迭代器为止。保证在第一次解引用迭代器之前,流已经读取。
ostream_iterator 上的操作
可以对任何具有输出操作符 >> 的类型定义 ostream_iterator。
ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec)
*out_iter++ = e; // the assignment writes this element to cout
cout << endl;
向 out_iter 赋值时,解引用符和递增符都可以忽略,因为 * 和 ++ 运算符不对 out_iter 作任何事。上面的循环可重写成:
for (auto e : vec)
out_iter = e; // the assignment writes this element to cout
cout << endl;
但是,推荐第一种形式。因为这种写法与其他迭代器类型的使用方式一致;且更易读。
通过调用 copy 打印 vec 中的元素:
copy(vec.begin(), vec.end(), out_iter);
cout << endl;
在类类型中使用流迭代器
istream_iterator<Sales_item> item_iter(cin), eof;
ostream_iterator<Sales_item> out_iter(cout, "\n");
// store the first transaction in sum and read the next record
Sales_item sum = *item_iter++;
while (item_iter != eof) {
// if the current transaction (which is stored in item_iter) has the same ISBN
if (item_iter->isbn() == sum.isbn())
sum += *item_iter++; // add it to sum and read the next transaction
else {
out_iter = sum; // write the current sum
sum = *item_iter++; // read the next transaction
}
}
out_iter = sum; // remember to print the last set of records
反向迭代器
反向迭代器中递增(和递减)的含义与原本的相反。
递增 ++it
反向迭代器可将迭代器移至上一个元素;递减 --it
将迭代器移至下一个元素。
可以通过调用 rbegin
、rend
、crbegin
和 crend
成员获取反向迭代器。这些成员返回反向迭代器,指向容器内的尾元素和首元素前一个位置。
vector<int> vec = {0,1,2,3,4,5,6,7,8,9};
// reverse iterator of vector from back to front
for (auto r_iter = vec.crbegin(); // binds r_iter to the last element
r_iter != vec.crend(); // crend refers 1 before 1st element
++r_iter) // decrements the iterator one element
cout << *r_iter << endl; // prints 9, 8, 7,... 0
通过向 sort 传递一对反向迭代器,实现 vec 的递减排序:
sort(vec.begin(), vec.end()); // sorts vec in ''normal'' order
// sorts in reverse: puts the smallest element at the end of vec
sort(vec.rbegin(), vec.rend());
反向迭代器需要递减运算符
只能从支持 ++
和 --
的迭代器定义反向迭代器。
不能从 forward_list 或流迭代器创建反向迭代器。
反向迭代器与其他迭代器的关系
string line = "FIRST,MIDDLE,LAST";
// find the first element in a comma-separated list
auto comma = find(line.cbegin(), line.cend(), ',');
cout << string(line.cbegin(), comma) << endl; // print "FIRST"
如果想要打印 line 中的最后一个单词:
// find the last element in a comma-separated list
auto rcomma = find(line.crbegin(), line.crend(), ',');
// WRONG: will generate the word in reverse order
cout << string(line.crbegin(), rcomma) << endl; // print "TSAL"
将 rcomma 转换成普通迭代器,能在 line 中正向移动。
可以通过调用 reverse_iterator 的 base
成员,返回其对应的普通迭代器。
// ok: get a forward iterator and read to the end of line
cout << string(rcomma.base(), line.cend()) << endl; // print "LAST"
从技术上来说,普通迭代器和反向迭代器之间的关系反映了左闭合区间的特性。
rcomma 和 rcomma.base() 必须生成相邻位置,而不是同一个位置。
10.4 泛型算法的结构
表10.5 迭代器类别 (Iterator Category)
迭代器 | 说明 |
---|---|
输入迭代器 | 只读,不写;单遍扫描,只能递增 |
输出迭代器 | 只写,不读;单遍扫描,只能递增 |
前向迭代器 | 可读写;多遍扫描,只能递增 |
双向迭代器 | 可读写;多遍扫描,可递增递减 |
随机访问迭代器 | 可读写;多遍扫描,支持全部迭代器运算 |
五类迭代器
输入迭代器 (input iterator):可以读取序列中的元素。输入迭代器必须提供:
- 相等和不相等运算符
==
、!=
,比较两个迭代器 - 前置和后置递增
++
,推进迭代器 - 解引用操作符
*
,读取元素;解引用只出现赋值的右边 - 箭头运算符
->
,等价于(*it).member
输入迭代器只能顺序使用。算法 find 和 accumulate 算法需要输入迭代器。
istream_iterator 是输入迭代器。
输出迭代器 (output iterator):可以看作输入迭代器功能的补充。输出迭代器必须提供:
- 前置和后置递增
++
,推进迭代器 - 解引用操作符
*
,只出现赋值的左边
用作目的位置的迭代器一般是输出迭代器。copy 函数的第三个参数是输出迭代器。
ostream_iterator 类型是输出迭代器。
前向迭代器 (forward iterator):可读写给定序列,只能在序列中沿一个方向移动。提供输入迭代器和输出迭代器的所有操作。
算法 replace 要求前向迭代器。forward_list 是前向迭代器。
双向迭代器 (bidirectional iterator):可正向或反向读写序列。支持前向迭代器所有操作,还支持前置和后置递减运算符 --
。
算法 reverse 要求双向迭代器。除了 forward_list,库容器提供的迭代器满足双向迭代器的要求。
随机访问迭代器 (random-access iterator):在常量时间内可访问序列中任意位置。支持双向迭代器所有操作,还支持以下操作:
- 关系运算符
<, <=, >, >=
,比较两个迭代器的相对位置。 - 在迭代器和整数值上的加减运算符
+, +=, -, -=
。 - 应用于两个迭代器的减法运算符
-
,生成迭代器之间的距离。 - 下标运算符
iter[n]
,等价于*(iter+n)
。
算法 sort 要求随机访问迭代器。array、deque、string 和 vector 的迭代器是随机访问迭代器,用于访问内置数组元素的指针也是。
算法形参模式
大多数算法具有如下 4 种形式之一:
alg(beg, end, other args);
alg(beg, end, dest, other args);
alg(beg, end, beg2, other args);
alg(beg, end, beg2, end2, other args);
接受单个目标迭代器的算法
写入输出迭代器的算法假定目标空间足够容纳写入的数据。
dest 形参表示目标迭代器。如果 dest 直接指向容器,那么算法将输出数据直接写入到容器已存在的元素中。更常见的是,dest 绑定到插入迭代器或 ostream_iterator。
接受第二个输入序列的算法
只接受 beg2(不接受 end2)的算法将 beg2 视为第二个输入范围的首元素。这些算法假定从 beg2 开始的范围至少与 beg 和 end 表示的范围一样大。
算法命名规范
一些算法使用重载传递谓词
接受谓词替代 <
或 ==
运算符的算法,和不接受其他参数的算法,一般是重载。
unique(beg, end); // uses the == operator to compare the elements
unique(beg, end, comp); // uses comp to compare the elements
_if 版本的算法
接受一个元素值的算法,一般有另一个不同名的版本,接受谓词替换值。接受谓词的算法有 _if 后缀。
find(beg, end, val); // find the first instance of val in the input range
find_if(beg, end, pred); // find the first instance for which pred is true
区分拷贝元素的版本和不拷贝的版本
默认情况下,重排元素的算法会将重排后的元素写回到给定输入范围。这些算法提供另一个版本,写入指定输出目标位置。这样的算法有 _copy 后缀。
reverse(beg, end); // reverse the elements in the input range
reverse_copy(beg, end, dest);// copy elements in reverse order into dest
一些算法同时提供 _copy 和 _if 版本。
// removes the odd elements from v1
remove_if(v1.begin(), v1.end(), [](int i) { return i % 2; });
// copies only the even elements from v1 into v2; v1 is unchanged
remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int i) { return i % 2; });
10.5 容器特定算法
于其他容器不同,list 和 forward_list 定义了几种算法作为成员。特别地,列表类型定义它们自己版本的 sort、merge、remove、reverse 和 unique。
表10.6 list 和 forward_list 成员算法
算法 | 说明(这些操作都返回 void) |
---|---|
lst.merge(lst2) | 将 lst2 中的元素合并到 lst。lst 和 lst2 必须是已排序的。merge 后,lst2 为空。使用 < 操作符。 |
lst.merge(lst2, comp) | 使用给定的比较运算符。 |
lst.remove(val) | 调用 erase 删除每个 == 给定值的元素。 |
lst.remove_if(pred) | 调用 erase 删除每个使一元谓词为真的元素。 |
lst.reverse() | 反转 lst 中元素的顺序。 |
lst.sort() | 使用 < 排序 lst 中的元素。 |
lst.sort(comp) | 使用给定比较操作排序 lst 中的元素。 |
lst.unique() | 调用 erase 删除相同元素的连续拷贝。使用 == |
lst.unique(pred) | 使用给定的二元谓词。 |
对于 list 和 forward_list,应优先使用列表成员版本,而不是通用算法。
splice 成员
splice 算法是列表数据结构特有的。
表10.7 list 和 forward_list 的 splice 成员的参数(lst.splice(args)
或 flst.splice_after(args)
)
参数 (args) | 说明 |
---|---|
(p, lst2) | p 是一个指向 lst 中的元素的迭代器,或一个 flst 中的元素之前的迭代器。 从 lst2 移动所有元素到 lst 中 p 之前,或移动到 flst 中 p 之后。删除 lst2 中的元素。 lst2 的类型必须与 lst 或 flst 相同,且不能是同一个列表。 |
(p, lst2, p2) | p2 是指向 lst2 有效位置的迭代器。将 p2 指向的元素移动到 lst,或将 p2 后面的元素移动到 flst。 lst2 可以是与 lst 或 flst 相同的链表。 |
(p, lst2, b, e) | b 和 e 必须表示 lst2 中的有效范围。移动 lst2 中给定范围的元素。 lst2 和 lst(或 flst)可以是相同的链表,但 p 不能指向给定范围内的元素。 |
链表特有的操作会改变容器
链表特有版本算法和通用版本算法之间的一个重要区别是:链表版本会改变底层的容器。