前言
这一章主要介绍泛型算法,关于常用的泛型算法,需要自己在写程序时用到才会更熟悉它的用法。在这一章还有一个特别重要的概念:lamda表达式,是一个可调用的对象。关于更多详细的知识,建议自行查看书籍。
最后,如果有什么理解不对的地方,希望大家不吝赐教,谢谢!
七、泛型算法
顺序容器只定义了很少的操作,但如果用户可能还希望做其他很多有用的操作:查找特定元素、替换或删除一个特定值、重排元素顺序等。标准库并未给每个容器都定义成员函数来实现这些操作,而是定义了一组泛型算法:称他们为“算法”,是因为他们实现了一些经典算法的公共接口,如排序和搜索;称他们是“泛型的”,是因为他们可以用于不同类型的元素和多种容器类型。
头文件
#include<algorithm>
#include<numeric> 一组数值泛型算法
一般情况下,这些算法并不会直接操作容器,而是遍历由两个迭代器指定的一个元素范围。
迭代器令算法不依赖于容器,但算法依赖于元素类型的操作
泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个令人惊讶但非常必要的编程假定:算法永远不会改变底层容器的大小,永远不会直接添加元素或删除元素。
虽然大多数算法遍历输入范围的方式相似,但他们使用范围中元素的方式不同,理解算法的最基本的方法就是了解他们是否读取元素、改变元素或是重排元素顺序。
只读元素
一些算法只会读取其输入范围内的元素,而从不改变元素。
- find
- count
- int sum=accumulate(vec.cbegin(),vec.cend(),0); //0为初值
- equal(v1.cbegin(),v1.cend(),v2.cbegin()); //对应元素若都相等,则返回true
那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
写容器元素的算法
一些算法将新赋予序列中的元素,当我们使用这类算法时,必须注意确保序列原大小至少不小于我们要求算法写入的元素数目。
- fill(vec.begin(),vec.end(),0); //将每个元素重置为0
- back_inserter:插入迭代器,定义在头文件iterator中,接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。返回的算是一个尾迭代器吧。
- copy:拷贝算法,auto ret=copy(begin(a1),end(a1),a2); //从a1到a2的拷贝,ret恰好指向拷贝到a2尾元素之后的位置
- replace:replace(ilst.begin(),ilst.end(),0,42); //将ilst中所有值为0的元素改为42
- repalce_copy:replace_copy(ilst.begin,ilst.end().back_inserter(ivec),0,42); //ilst没有改变,ivec就是原先改变后的结果
重排容器元素的算法
这些算法会重排容器中元素的顺序,一个明显的例子就是sort。调用sort会使输入序列中的元素按照<运算符来实现排序。
定制操作
(1)向算法传递函数
为了按长度重排vector,我们将使用sort的第二个版本,此版本是重载过的,它接受第三个参数,此参数是一个谓词。
谓词
分为两类:一元谓词和二元谓词
接受谓词参数的算法对输入序列中的元素调用谓词,因此,元素类型必须能转换为谓词的参数类型。
//比较函数,用来按长度排序单词 bool isShorter(const string &s1,const string &s2) { return s1.size()<s2.size(); } //按长度由短至长排序words sort(words.begin(),words.end(),isShorter);
lambda表达式
我们可以向一个算法传递任何类别的可调用对象。对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。即,如果e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一个或多个参数的列表。
目前为止,我们使用过的仅有的两种可调用对象是函数和函数指针。还有其他两种对象:重载了函数调用运算符的类,以及lamda表达式。
一个lamda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lamda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lamda可能定义在函数内部,且必须使用尾置返回来指定返回类型。
[capture list](parameter list)->return type {function body} capture:是一个lamda所在函数中定义的局部变量的列表(通常为空)
如果lamda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void。
使用捕获列表
例如:我们的lamda会捕获sz,并只有单一的string参数,其函数体会将string的大小与捕获的sz的值进行比较:
[sz](const string &s) {return a.size()>=sz;};
lamda以一对[]开始,我们可以在其中提供一个以逗号分隔的名字列表,这些名字都是它所在函数中定义的。由于lamda捕获sz,因此lamda的函数体可以使用sz。
一个lamda只有在其捕获列表中捕获一个它所在函数体中的局部变量,才能在函数体中使用该变量。
捕获列表只用于局部非static变量,lamda可以直接使用局部static变量和它所在函数之外声明的名字。
一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用。
隐式捕获
让编译器根据lamda体中的的代码来推断我们要使用哪些变量,为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。当我们混合使用隐式捕获和显示捕获时,捕获列表中的第一个元素必须是一个&或=。此符号指定了默认捕获方式为引用或值。[&,c]或[=,&c]
可变lamda
默认情况下,对于一个值被拷贝的变量,lamda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表加上关键字mutable。因此,可变lamda能省略参数列表。
void fcn3() { size_t v1=42; //局部变量 //f可以改变它所捕获的变量的值 auto f=[v1]()mutable{return ++v1}; v1=0; auto j=f(); //j为43 }
一个引用捕获的变量是否可以修改依赖于此引用指向的是一个const类型还是一个非const类型:
void fcn4() { size_t v1=42; //局部变量 //v1是一个非const变量的引用 //可以通过f2中的引用来改变它 auto f2=[&v1]{return ++v1;}; v1=0; auto j=f2(); //j为1 }
指定lamda返回类型
默认情况下,如果一个lamda体包含return之外的任何语句,则编译器假定此lamda返回void,与其他返回void的函数类似,被推断返回void的lamda不能返回值。当我们需要为一个lamda定义返回类型时,必须使用尾置返回类型:
transform(vi.begin(),vi.end,vi.begin(),[](int i)->int{if(i<0) return -i;else return i});
参数绑定
对于那种只有在一两个地方使用的简单操作,lamda表达式是最有用的。如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lamda表达式。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好。
标准库bind函数
需要加头文件#include<functional>,可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
auto newCallable=bind(callable,arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数,即当我们调用newCallable时,newCallable会调用callable,并传递给arg_list中的参数。arg_list中的参数可能包含形如_n的名字,_1为newCallable的第一个参数,_2为第二个参数,一次类推。
auto wc=find_if(words.begin(),words.end(),[sz](const string &a)); //等价于 auto wc=find_if(words.begin(),words.end(),bind(check_size,_1,sz));
使用placeholders名字
名字_n都定义在一个名为placeholders的命名空间中,需要加上:using namespace std::placeholders;
bind的参数
//g是一个有两个参数的可调用对象 auto g=bind(f,a,b,_2,c,_1);
即g(_1,_2)会映射为f(a,b,_2,c,_1),则调用g(X,Y)会调用f(a,b,Y,c,X)
用bind重排参数顺序
//按单词长度由短至长排序 sort(words.begin(),words.end,isShorter); //按单词长度由长至短排序 sort(words.begin(),words.end(),bind(isShorter,_2,_1));
泛型算法结构
算法所要求的迭代器操作可以分为5个迭代器类别。
- 输入迭代器:只读不写,单遍递增扫描
- 输出迭代器:只写不读,单遍递增扫描
- 前向迭代器:可读写,多遍递增扫描
- 双向迭代器:可读写,多遍双向扫描
- 随机访问迭代器:可读写,支持全部迭代器运算