C++ 学习笔记之(10) - 泛型算法和迭代器
标准库容器定义的操作结合非常小,为了实现更丰富的功能,标准库定义了一组反省算法。
概述
大多数算法定义在头文件algorithm
中,头文件numeric
中定义了一组数值泛型算法
- 迭代器令算法不依赖于容器,但依赖于元素类型的操作,比如元素类型的
==
运算符 - 泛型算法本身不会执行容器的操作,他们只会运行于迭代器之上,故算法永远不会改变底层容器的大小
初识泛型算法
除了少数列外,标准库算法都对一个范围内的元素进行操作。此元素范围被称为输入范围
。接受输入范围的算法总是使用前两个参数表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。
只读算法
某些算法只读取输入范围内的元素,而从不改变元素
accumulate
:定义在头文件numeric
中,求和算法, 第三个参数的类型决定了函数使用那个加法运算符以及返回值的类型。// 对 vec 中的元素求和,初识为 0 int sum = accumulate(vec.cbegin(), vec.cend(), 0); // 链接 v 中所有 string 元素 string num = accumulate(v.cbegin(), v.cend(), string(""));
equal
:确定两个序列是否保存相同的值// roster2 中的元素数目应该至少与 roster1 一样多 equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长
写容器元素的算法
某些算法将新值赋予序列中的元素
算法并不会执行容器操作,故不可能改变容器大小
一些算法从两个序列中读取元素,构成这两个序列的元素可以来自于不同的容器类型
若第二个序列是第一个序列的子集,则程序会产生严重错误
插入迭代器(insert iterator):一种向容器中添加元素的迭代器
拷贝算法:另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法
// 把 a1 的内容拷贝到 a2, ret 指向拷贝到 a2 的尾元素之后的位置 auto ret = copy(begin(a1), end(a1), a2); // 将所有值为 0 的元素改为 42 replace(ilst.begin(), ilst.end(), 0, 42);
重排容器元素的算法
某些算法会重排容器中元素的顺序, 比如sort
- 举例:消除文本中重复单词
- 首先将
vector
排序,使用sort
- 然后使用
unique
算法重排vector
,使得不重复的单词出现在vector
前面,返回指向不重复值范围末尾的迭代器 - 使用容器操作真正删除元素
- 首先将
- 标准库算法对迭代器而不是容器操作,故算法不能(直接)添加或删除元素
定制操作
很多算法会比较输入序列中的元素,默认使用元素类型的<
或==
运算符,也可以使用自定义操作代替
向算法传递函数
谓词:可调用的表达式,返回结果是一个能用作条件的值,接受谓词参数的算法对输入序列中的元素调用谓词
- 一元谓词:接受单一参数
- 二元谓词:接受两个参数
// 比较函数,用来按长度排序单词 bool isShorter(const string &s1, const string &s2) { return s1.size() < s2.size(); } // 按长度由断至长排序 words sort(words.begin(), words.end(), isShorter);
statble_sort
:未定排序算法,可维持相等元素的原有顺序
lambda
表达式
可调用对象:可以对其使用调用运算符的对象,比如函数和函数指针
lambda
表达式:可调用的代码单元,也可理解为未命名的内联函数。形式:
[capture list](parameter list) -> return type { function body }
可忽略参数列表和返回类型,但必须永远包含捕获列表和函数体
若
lambda
的函数体包含任何单一return
语句之外的内容,且未指定返回类型,则返回void
lambda
不能有默认参数, 且实参和形参类型必须匹配
// 此 lambda 表达式等驾驭 isShorter 函数 [](const string &a, const string &b) { return a.size() < b.size(); }
lambda
只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量捕获列表只用于局部非
static
变量,lambda
可以直接使用局部static
变量和它所在函数之外声明的名字
// 此 lambda 用来查找第一个长度大于等于 sz 的元素 auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; });
捕获变量可以是值或引用, 值捕获发生在
lambda
创建时拷贝,不是调用时拷贝隐式捕获:使用
&
表示采用该引用捕获方式,=
表示采用值捕获方式
// 混合使用隐式捕获和显示捕获,捕获列表总第一个元素必须是 & 或 =, 指定默认捕获方式是引用或值 void iggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ') { // for_each 算法接受可调用对象,并对输入序列中每个元素调用次对象 // os 隐式捕获, 引用捕获; c 显示捕获, 值捕获方式 for_each(words.begin(), words.end(), [&, c](const string &s) { os << s << c; }); // os 显示捕获, 引用捕获方式; c 隐式捕获, 值捕获方式 for_each(words.begin(), words.end(), [=, &os](const string &s) { os << s << c; }); }
- 可变
lambda
:若想改变被捕获变量的值,则必须在参数列表首加上关键字mutable
size_t v1 = 42; // 局部变量 auto f = [v1]() mutable { return ++v1; }; // f 可以改变它所捕获变量的值 v1 = 0; auto j = f(); // j 为 43, 记住值捕获发生在 lambda 创建时拷贝, 若是引用捕获,则结果为 1
- 若
lambda
无法推断返回类型是,可指定返回类型
// 将序列 vi 中的每个负数替换为其绝对值, transform 算法对输入序列中每个元素调用可调用对象,并将结果写到目的位置 transform(vi.being(), vi.end(), vi.begin(), [](int i)-> int { if (i < 0) return -i; else return i; });
参数绑定
若lambda
捕获列表为空,通常可用函数代替。 但对于捕获局部变量的lambda
,函数就不能轻易替换, 因为要解决如何传参的问题。比如find_if
算法的可调用对象必须接受单一参数,lambda
可使用捕获局部变量,但函数就无法替换。
标准库 bind
函数
定义在头文件functional
中, 可看做通用函数适配器, 接受一个可调用对象,生成新的可调用对象来适应原对象的参数列表
形式
auto newCallable = bind(callable, arg_list);
当调用newCallable
时,newCallable
会调用callable
,并传递给它arg_list
中的参数newCallable
为可调用对象arg_list
为参数列表,对应给定的callable
的参数。参数可能包含_n
名字,表示占位符,数值n
表示生成的可调用的对象中参数的位置。_1
表示newCallable
的第一个参数
bool check_size(const string &s, string::size_type sz) { return s.size() >= sz; } // 将基于 lambda 的 find_if 调用改为使用 check_size 的版本 auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));
placeholders命名空间
名字_n
都定义在placeholders
命名空间,而此命名空间定义在std
命名空间中,为std::placeholders::_n
- 使用某命名空间:
using namespace std::placeholders;
可使用该命名空间中所有名字
bind
的参数
如前所述,可使用bind
修正参数的值,还可以使用bind
绑定给定可调用对象中的参数或重新安排其顺序
// 假设 f 是一个可调用对象,有 5 个参数; g 是一个有两个参数的可调用对象
auto g = bind(f, a, b, _2, c, _1);
g(x, y); // 实际调用 f(a, b, _2, c, _1); 传递给g的参数按位置绑定到占位符,x绑定到_1,y 绑定到_2。
bind
无法绑定引用参数
bind
拷贝其参数,但某些参数无法被拷贝, 比如ostream
, 若传递引用,可以使用标准库ref
函数
ostream &print(ostream &os, const string &s, char c) { return os << s << c; }
// 等价于上述采用 lambda 的 for_each
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));
再探迭代器
除了为每个容器定义的迭代器外,标准库在头文件iterator
中还定义其他迭代器
- 插入迭代器(insert iterator):与容器绑定,可向容器插入元素
- 流迭代器(stream iterator):与输入输出流绑定,可用来遍历所关联的
IO
流 - 反向迭代器(reverse iterator):移动方向往后,除了
forward_list
之外的标准库容器都有 - 移动迭代器(move iterator):用来移动元素
插入迭代器
插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素
back_inserter
:创建一个使用push_back
的迭代器(容器支持push_back
)front_inserter
:创建一个使用push_front
的迭代器(容器支持push_front
)inserter
:创建一个使用insert
的迭代器, 第二个参数是一个指向给定容器的迭代器list<int> lst = {1, 2, 3, 4}; auto it = inserter(lst, lst.begin()); *it = 0; // lst 为 {0, 1, 2, 3, 4}, 等同于 it = c.insert(it, val); ++it; it指向原元素 list<int> lst2, lst3; copy(lst.cbegin(), lst.cend(), front_inserter(lst2)); //拷贝完成后,lst2为{4, 3, 2, 1, 0} copy(lst.cbegin(), lst.cend(), insert(lst3, lst3.begin())); // lst3 为 {0, 1, 2, 3, 4}
iostream
迭代器
虽然iostream
不是容器,但标准库定义了可用于这些IO
类型对象的迭代器
istream_iterator
读取输入流。允许懒惰求值,即标准库不保证迭代器立即从流读取数据,但可以保证,在使用之前,已经从流中读取完成
- 创建
istream_iterator
时,可以将其绑定到一个流 - 默认初始化,相当于创建了一个可以当做尾后值使用的迭代器
istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl; // 计算从标准输入读取的值的和
ostream_iterator
向输出流写数据,必须绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator
ostream_iterator<int> out_iter(cout, " ");
for(auto e : vec)
*out_iter++ = e; // 赋值语句实际上将元素写到 cout, 也可写成 out_iter = e, 但推荐第一种
cout << endl;
任何定义了输入运算符>>
的类型都能创建istream_iterator
对象,类似,有输出运算符<<
,就能定义ostream_iterator
反向迭代器
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器
- 反向迭代器需要递减运算符
泛型算法结构
任何算法的最基本特性是要求其迭代器提供那些操作
5类迭代器
输入迭代器
读取序列中的元素
- 用于比较两个迭代器的相等和不相等(
==
、!=
) - 用于推进迭代器的前置和后置递增运算(
++
) - 用于读取元素的解引用运算符(
*
);解引用只会出现在赋值运算符的右侧 - 箭头运算符(
->
), 等价于(*it.member
), 即,解引用迭代器,并提取对象的成员
输出迭代器
可看做是输入迭代器功能的补集,只写不读元素。只能赋值一次,用於单遍扫描算法
- 用于推进迭代器的前置和后置递增运算(
++
) - 解引用运算符(
*
),只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)
前向迭代器
可读写元素,只能沿单一方向移动,支持所有输入输出迭代器的操作,可进行多遍扫描,即可保存前向迭代器的状态
双向迭代器
可正向或反向读写元素,支持所有钱箱迭代器的操作,并支持--
运算符
随机访问迭代器
提供在常量时间内访问任意元素的能力,支持双向迭代器的所有功能
- 用于比较两个迭代器相对位置的运算符(
<
,<=
,>
,>=
) - 迭代器和整数值的加减运算(
+
,+=
,-
,-=
),结果是迭代器位置的移动 - 用于两个迭代器的减法运算符(
-
), 得到两个迭代器的距离 - 下标运算符(
iter[n]
), 与*(iter[n])
等价
特定容器算法
对于list
和forward_list
,应该优先使用成员函数版本的算法,而不是通用算法
splice 成员
链表数据结构特有,故不需要通用版本
链表特有版本的算法会改变底层容器
结语
标准库定义了大约100个类型无关的对序列进行操作的算法。序列可以是标准库容器类型中的元素、一个内置数组或者是通过读写一个流来生成的。算法通过在迭代器上进行操作来实现类型无关
根据支持的操作不同,迭代器分为输入、输出、前向、双向以及随机访问迭代器五类
算法从不直接改变他们所操作的序列的大小,除了链表特有版本的算法
虽然算法不能向序列添加元素,但插入迭代器可以