参考资料:
- 《C++ Primer》第5版
- 《C++ Primer 习题集》第5版
10.1 概述(P336)
大多数算法定义在头文件 algorithm
中,头文件 numeric
中也定义了一组数值泛型算法。
一般情况下,算法不直接操作容器,而是通过迭代器来进行操作。比如标准库算法 find
,前两个参数是表示元素范围的迭代器,第三个参数是一个值。find
将范围内的每个元素与给定值相比较,返回第一个与给定值相等的元素的迭代器,如果范围中无匹配元素,则返回第二个参数:
int val;
auto result = find(vec.cbegin(), vec.cend(), val);
由于指针就像内置数组上的迭代器一样,我们可以用 find
在内置数组上查找值:
int *result = find(begin(arr), end(arr), val);
算法如何工作
只要有一个迭代器用来访问元素,find
就完全不依赖容器类型。
迭代器令算法不依赖于容器
利用迭代器解引用实现元素访问;使用迭代器递增运算符可以移动到下一个元素;使用迭代器比较可以判断是否到达序列末尾。
但算法依赖于元素类型的操作
find
使用元素类型的 ==
运算符,其他算法可能要求 <
运算符。
关键概念:算法永远不会执行容器操作!算法只会运行于迭代器之上。算法可能改变容器中元素的值,也可能在容器中移动元素,但永远不会直接添加或删除元素。
尽管一类特殊的迭代器——插入器(inserter)可以在底层容器上执行插入操作,但算法自身不会执行这样的操作。
简单来说,算法只会执行迭代器操作和元素操作。至于真正操作的是什么类型的迭代器,算法并不关心。
10.2 初识泛型算法(P338)
标准库算法大多对一个范围内的元素进行操作。接受输入范围的算法总是用前两个参数表示这个范围,分别指第一个元素和尾元素之后的位置。
虽然大多数算法遍历输入范围的方式类似,但它们使用范围中元素的方式不同。理解算法最基本的方法就是了解它们是否读取元素、改变元素或是重排元素。
10.2.1 只读算法(P338)
一些算法只会读取范围内的元素,如 find
和 count
。另一个只读算法是 numeric
中的 accumulate
,它接受三个参数,前两个指出需要求和的范围,第三个参数是和的初值:
// vec是一个整数序列
int sum = accumulate(vec.cbegin(), vec.cend(), 0);
accumulate
的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。
算法和元素类型
accumulate
将第三个参数作为求和起点,这要求将元素类型加到和上是可行的:
string sum = accumulate(v.cbegin(), v.cend(), string(""));
string sum = accumulate(v.cbegin(), v.cend(), ""); // 错误,const char*没有+运算符
操作两个序列的算法
另一个只读算法是 equal
,用于确定两个序列是否保存相同的值。此算法接受三个参数:前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素:
equal(roster1.cbegin(), roster1.cend(), roster.cbegin());
equal
只会一一比较元素,所以假定两个序列一样长。两个序列的元素类型不必相同,只要能用 ==
比较即可。
10.2.2 写容器元素的算法(P339)
fill
算法接受一对迭代器表示一个范围,还接受一个值并将这个值赋予输入序列中的每个元素:
fill(vec.begin(), vec.end(), 0); // 将每个元素置0
算法不检查写操作
fill_n
用一个迭代器和一个长度表示范围,算法假定容器足够容纳要写入的元素。
介绍back_inserter
当我们通过插入迭代器(insert iterator)赋值时,一个与赋值号右侧相等的元素被添加到容器中。
back_inserter
定义在头文件 iterator
中,接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器:
vector<int> vec; // 空向量
auto it = back_inserter(vec);
fill_n(it, 10, 0); // 通过插入迭代器向vec中添加元素
拷贝算法
copy
算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置,将输入范围中的元素拷贝到目的序列中。目的序列至少要与输入序列包含一样多的元素:
int arr1[] = { 0,1,2,3,4,5,6,7,8,9 };
int arr2[20] = {};
copy(begin(arr1), end(arr1), begin(arr2));
copy
返回迭代器,指向拷贝的最后一个元素之后的位置。
多个算法提供拷贝版本:
vector<int> vec1 = { 0,1,0,1,0,1,0,1,0,1 };
vector<int> vec2;
// vec1内容不变
replace_copy(vec1.begin(), vec1.end(), back_inserter(vec2), 0, 1);
replace(vec1.begin(), vec1.end(), 0, 1);
10.2.3 重排容器元素的算法(P342)
sort
利用元素类型的 <
运算符来实现排序。
消除重复单词
假设我们有如下输入,并希望消除重复单词:
the quick red fox jumps over the slow red turtle
为了消除重复单词,首先将 vector
排序,然后使用 unique
算法重排 vector
使得不重复的元素出现在 vector
的开始部分。如果我们想要删除重复元素,还需要使用 vector
的 erase
成员:
void elimDups(vector<string> &words) {sort(words.begin(), words.end());// unique返回不重复区域之后的位置auto end_unique = unique(words.begin(), words.end());words.erase(end_unique, words.end());
}
10.3 定制操作(P344)
10.3.1 向算法传递函数
如果我们希望将单词先按单词长度排序,再按字典顺序排序,则可以使用另一个版本的 sort
,它接受第三个参数——一个谓词(predicate)。
谓词
谓词是一个可调用的表达式,其返回结果是一个能作为条件的值。
标准库算法使用的谓词分两类:一元谓词(unary predicate)和二元谓词(binary predicate)。接受谓词参数的算法对输入序列中的元素调用谓词。
接受一个二元谓词参数的 sort
版本用这个谓词代替 <
来比较元素:
bool isShorter(const string &s1, const string &s2) {return s1.size() < s2.size();
}sort(words.begin(), words.end(), isShorter);
排序算法
为了保持相同长度的单词按照字典序排列,我们可以使用 stable_sort
算法。
10.3.2 lambda
表达式(P345)
假设我们要知道大于等于一个给定长度的单词有多少,并且只打印这些符合条件的单词。我们可以使用 find_if
算法,它接受一对迭代器表示范围,接受一个一元谓词,对每个元素调用这个谓词,返回第一个使谓词非 0 的元素的迭代器。
介绍lambda
一个对象或一个表达式,如果可以对其使用调用运算符,则称其为可调用的。可调用对象有四种:函数、函数指针、重载了调用运算符的类、lambda
表达式。
lambda
表达式是一个可调用的代码单元,可以将其理解为一个未命名的内联函数。lambda
表达式具有返回类型、参数列表、函数体。与函数不同,lambda
可以定义在函数内部:
[capture list] (parameter list) -> return type { function body }
其中,capture list
(捕获列表)是 lambda
所在函数中定义的局部变量列表(通常为空)。lambda
表达式必须使用尾置返回来指定返回类型。
我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体:
auto f = [] {return 42; };cout << f() << endl;
lambda
忽略括号和参数列表等价于指定一个空参数列表,如果忽略返回类型,lambda
根据函数体的代码推断返回类型:如果函数体包含 return
语句,则返回类型从返回的表达式类型推断而来,否则返回类型 void
。
向lambda
传递参数
lambda
不能有默认参数:
[](const string &a, const string &b){return a.size(), b.size(); };
使用捕获列表
lambda
如果想要使用所在函数中的局部变量,就需要在捕获列表中指明:
// sz是指定的单词长度
[sz](const string &a){return a.size() > sz; };
调用find_if
// 获得一个迭代器,指向第一个满足size()>=sz的元素
auto wc = find_if(words.begin(), words.end(), [sz](const string &a){return a.size() > sz; });
for_each
算法
for_each
接受一个可调用对象,并对输入序列中每个元素调用此对象:
for_each(wc, words.end(),[](const string &s) {cout << s << ' '; });
捕获列表只用于局部非
static
变量,lambda
可以直接使用局部static
变量和它所在函数之外声明的名字。
10.3.3 lambda
捕获和返回(P349)
当定义一个 lambda
时,编译器生成一个与之对应的、新的、未命名的类类型。当我们把 lambda
传递给函数时,目前可以理解为同时定义了一个新类型和该类型的一个对象,传递给函数的参数就是这个对象。
默认情况下,从 lambda
生成的类都包含对应捕获变量的数据成员,并在对象创建时被初始化。
值捕获
值捕获的前提是变量可以拷贝,需要注意的是,被捕获变量的值在 lambda
创建时被拷贝,而非调用时拷贝:
int x = 0;
auto f = [x] {return x; };
x = 1;
cout << f() << endl; // 输出结果为0
引用捕获
int x = 0;
auto f = [&x] {return x; };
x = 1;
cout << f() << endl; // 输出结果为1
如果采用引用方式捕获一个变量,就必须保证被引用对象在 lambda
执行时是存在的。
隐式捕获
我们可以在捕获列表中写一个 =
或 &
,让编译器根据 lambda
体中的代码来推断我们需要哪些变量:
auto wc = find_if(words.begin(), words.end(), [=](const string &a){return a.size() > sz; });
隐式捕获和显式捕获可以混用,但必须保证捕获列表中的第一个元素是 &
或 =
,且显式捕获的变量必须使用与隐式捕获不同的方式。
此处有个图片
可变lambda
默认情况下,对于一个值捕获的变量,lambda
不会改变其值。如果我们希望改变被捕获变量的值,就必须在参数列表后加上 mutable
关键字。因此,可变 lambda
不能忽略参数列表:
int x = 0;
auto f = [x] ()mutable {return ++x; };
x = 10;
cout << f() << endl; // 输出为1
指定lambda
返回类型
标准库 transform
算法接受三个迭代器和一个可调用对象,前两个迭代器表示一个输入范围,第三个迭代器表示目的位置。算法对输入范围内的每个对象调用可调用对象,并将结果写到目的位置:
vector<int> v = { -1, 2, -3, 4 };
transform(v.begin(), v.end(), v.begin(),[](int i) {return i < 0 ? -i : i; }); // 正确
transform(v.begin(), v.end(), v.begin(),[](int i) {if (i < 0) i = -i; return i; }); // 正确
transform(v.begin(), v.end(), v.begin(),[](int i)->int {if (i < 0) i = -i; return i; }); // 正确
10.3.4 参数绑定(P354)
如果我们需要在多个地方使用相同的操作,或是一个操作需要很多语句才能完成,我们最好定义一个函数,而非使用 lambda
表达式。
如果 lambda
的捕获列表为空,通常可以用函数替代它;如果捕获列表非空,用函数替代就不太容易(比如前面提到的 find_if
函数只接受一元谓词,而用函数替代 lambda
则至少需要两个参数)。
标准库bind
函数
bind
函数定义在头文件 functional
函数,接受一个可调用对象,生成一个新的可调用对象:
auto newCallable = bind(callable, arg_list);
arg_list
是逗号分隔的参数列表,对应给定 callable
的参数。当我们调用 newCallable
时,newCallable
会调用 callable
,并传递给它 arg_list
中的参数。
arg_list
中可能包含形如 _n
的名字,这些参数是“占位符”,表示 newCallable
的参数,例如:_1
为 newCallable
的第一个参数。
绑定check_size
的sz
参数
void print(const int &x, const int &y) {cout << x << ' ' << y;
}auto print1 = bind(print, _1, 6);
print1(1); // 输出1 6
auto print2 = bind(print, 6, _3);
print2(0, 1, 2); // 输出6 2
使用placeholder
名字
名字 _n
都定义在命名空间 placeholders
中,而 placeholders
又定义在命名空间 std
中:
using namespace std::placeholders;
// 单纯的using namespace std不可行
bind
参数
我们还可以用 bind
给可调用对象中的参数调整顺序:
sort(words.begin(), words.end(), isShorter); // 升序排序
sort(words.begin(), words.end(), bind(isShorter, _2, _1)); // 降序排序
绑定引用参数
假设我们有下面的 lambda
表达式:
// os是一个局部变量,引用一个输出流
// c是一个局部变量,类型为char
[&os, c] (const string &s) {os << s << c; };
我们试着用 bind
来实现类似的功能:
void print(ostream &os, const string &s, char c){os << s << c;
}bind(print, os, _1, ','); // 错误,os不能拷贝
bind(print, ref(os), _1, ','); // 正确
如果我们不希望 bind
拷贝参数,必须使用 ref
函数或 cref
函数。
直白地说,就是先将参数传递给
newCallable
,这一步是值传递还是引用传递由bind
函数决定;然后,newCallable
将参数传递给callable
,这一步是值传递还是引用传递由callable
决定。