标准库定义了一组泛型算法来实现一些有用的操作:查找特定元素、重排元素顺序等。
泛型算法可以用于不同类型的元素和多种容器类型。
1.概述
大多数泛型算法都定义在头文件algorithm中,部分算法在头文件numeric中定义。
一般情况下,这些算法不会直接操作容器,而是遍历两个迭代器指定的元素范围来进行操作。
标准库算法find帮助我们查找序列中是否包含某个特定值。
string val = "value";
auto result = find(lst.cbegin(),lst.cend(),val);
//find返回第一个等于val的元素的迭代器,否则返回end()
指针就像内置数组上的迭代器一样,允许用find在数组中查找值。
int arr[] = {1,2,3,4,5,6,7,8);
int val = 5;
int* result = find(begin(arr),end(arr),val);
//begin和end函数返回的是指针
find算法还能在序列的子范围中查找给定值。
auto result = find(arr+1,arr+4,val);
泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。
2. 初识泛型算法
大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同。
泛型算法对容器的空间并无要求,对元素个数有要求。
2.1、只读算法
一些算法只会读取其输入范围内的元素,而从不改变元素。
accumulate算法,定义在头文件numeric中,用于求和。
int sum = accumulate(vec.cbegin(),vec.cend(),0);
//前两个参数表示求和元素的范围,第三个参数是和的初值
accumulate的第三次参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。
序列中元素的类型必须与第三个参数匹配,或者能够转换为第三个参数的类型。
对于只读而不改变元素的算法,通常最好使用cbegin( )和cend( )。
equal算法,用于确定两个序列是否保存相同的值。
equal(arr1.cbegin(),arr1.cend(),arr2.cbegin());
//前两个参数表示第一个序列中元素的范围,第三个参数表示第二个序列的首元素
equal通过迭代器来完成操作,因此两个不同类型的元素也能进行比较。
equal基于一个重要的假设:第二个序列至少与第一个序列一样长。
2.2、写容器元素的算法
调用该类算法时,牢记算法不会执行容器操作,因此它们本身不具有改变容器的大小的权限。
算法fill将给定值赋予输入序列中的每个元素。
fill(vec.begin(),vec.end(),0); //将每个元素重置为0
fill(vec.begin(),vec.begin()+vec.size()/2,10); //将容器中的子序列置为10
函数fill_n将给定值赋予迭代器指向的元素开始的指定个元素。
fill_n(vec.begin(),5,0); //将开头的5个元素置为0
//函数假定写入指定个元素是安全的,不超出序列
在一个空容器上调用fill_n(或类似的写元素的算法),该行为是未定义的。
向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。
使用插入迭代器能保证算法有足够元素空间来容纳输出数据。
back_inserter返回插入迭代器的函数,定义在头文件iterator中。
back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。
auto it = back_insert(vec); //it是一个插入迭代器
通常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。
拷贝算法向目的位置迭代器指向的输出序列中的元素写入数据。
int a1[] = {1,2,3};
int a2[3]; //a2与a1大小一致或大于
auto ret = copy(begin(a1),end(a1),a2); //把a1的内容拷贝给a2
copy返回的其目的位置迭代器(递增后)的值,即ret指向a2尾元素之后的位置。
replace算法读取一个序列,并将其中所有等于给定值的元素都改为另一个值。
replace(lst.begin(),lst.end(),0,42); //将所有的0替换成42
replace_copy算法接受额外第三个迭代器参数,指出调整后序列的保存位置。
replace_copy(lst.begin(),lst.end(),back_inserter(vec),0,42);
//lst并未改变,将改变后的lst拷贝到vec中
2.3、重排容器元素的算法
标准库算法unique只覆盖相邻的重复元素,使得不重复元素出现在序列开端部分。
unique返回的迭代器只想最后一个不重复元素之后的位置,删除操作由erase来执行。
auto endue = unique(words.begin(),words.end());
执行完unique后,words的大小并未改变,容器大小不变。
标准库算法对迭代器进行操作。因此,算法不能直接添加或删除元素、改变容器大小。
3.定制操作
3.1、向算法传递函数
算法的参数是一个函数,该参数被称为谓词。
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。
谓词通过参数个数分为一元谓词和二元谓词,输入序列中元素类型必须能转换为谓词的参数类型。
//sort的重载版本
bool isshorter(const string& s1,const string& s2)
{
return s1.size() < s2.size();
}
sort(words.begin(),words.end().isshorter); //isshorter是个谓词
stable_sort算法是一种稳定排序算法,维持相等元素的原有顺序。
3.2、lambda表达式
谓词的参数个数被严格限制,当我们需要更多参数来进行操作时,可以使用lambda表达式。
一个lambda表达式表示一个可调用的代码单元,可以理解为一个未命名的内联函数。
lambda表达式与其他函数不同,lambda可能定义在函数内部。
[a](b)-> c {d}
//a是lambda所在函数中定义的局部变量的列表(一般为空)
//b是该内联函数的参数列表
//c是返回类型(lambda必须使用尾置返回来指定返回类型)
//d是函数体
C++允许忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。
auto f = [] {return 2;};
如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。
如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void。
与普通函数不同,lambda不能有默认参数,lambda调用的实参数目永远与形参数目相等。
stable_sort(words.begin(),words.end(),
[](const string& a,const string& b)
{return a.size() < b.size();});
空捕获列表表明此lambda不使用它所在函数中的任何局部变量。
一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和它所在函数之外声明的名字。
3.3、lambda捕获和返回
当定义一个lambda是,编译器生成一个与lambda对应的新的(未命名)类类型。
默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。
类似参数传递,变量的捕获方式可以是值或引用。
采用值捕获的前提是变量可以被拷贝。
与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。
size_t v1 = 42;
auto f = [v1] {return v1;};
v1 = 0;
auto j = f(); //j为42
定义lambda时可以采用引用方式捕获变量。
size_t vv1 = 42;
auto f2 = [&v1] {return v1;};
v1 = 0;
auto j = f2(); //j为0
在函数体使用此变量时,实际上使用的是引用所绑定的对象。
采用引用方式捕获一个变量,必须确保被引用的对象在lambda执行的时候是存在的。
为了隐式指示编译器推断捕获列表,应在捕获列表中写一个&或=。
[&] //函数中的未捕获的变量采用捕获引用方式
[=] //采用值捕获方式
当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是&或=(隐式放前面)。
[=,&os]
[&,val]
当混合使用隐式和显式时,显式捕获的变量必须使用与隐式捕获不同的方式。
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。
想改变一个被捕获的变量的值,就必须在参数列表后加上关键字mutable。
size_t v1 =42;
auto f = [v1] ()mutable {return ++v1;};
v1 = 0;
auto j = f(); //j = 43
一个引用捕获的变量是否能被修改依赖于此引用指向的是一个const类型还是非const类型。
如果lambda表达式的函数体含有if语句,需定义返回类型,否则会产生编译错误。
3.4、参数绑定
一般来说,lambda表达式只适用于在一两个地方使用的简单操作。
标准库函数bind,定义在functional头文件中。
bind函数接受一个可调用对象,生成一个新的可调用对象来匹配原对象的参数列表。
auto new = bind(a,list);
//new是可调用对象
//list是一个逗号分隔的参数列表,对应着a的参数
list中的参数可能包含形如_n的名字,其中n是个整数,这些参数是“占位符”,表示new的参数。
数值n表示生成的可调用对象中参数的位置。
auto check1 = bind(check,_1,_2); //_1\_2表示的传递顺序
string s = "hello";
string s1 = "good";
bool b1 = check1(s1,s); //将s1,s2传递给check
名字_n都定义在名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间中。
using std::placeholders::_1;
//使用using声明来简化
对每个占位符名字,我们都必须提供一个单独的using声明。
bind的运用:可以用bind绑定给定可调用对象中的参数或重新安排参数顺序、修改参数的值。
sort(words.begin(),words.end(),isshorter); //从短到长
sort(words.begin(),words.end(),bind(isshorter,_2,_1); //从长到短
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。
通过使用标准库ref函数来传递那些无法被拷贝的参数,ref函数返回一个可拷贝的对象。
bind(print,ref(os),_1,' ');
标准库的cref函数生成一个保存const引用的类。
4.再探迭代器
标准库在头文件iterator定义了插入迭代器、流迭代器、反向迭代器、移动迭代器。
4.1、插入迭代器
插入迭代器是一种迭代器适配器,它生成一个能实现向给定容器添加元素的迭代器。
插入迭代器存在解引用、递增、递减操作,但这些操作并不会对迭代器造成什么影响。
back_inserter | 创建一个使用push_back的迭代器 |
front_inserter | 创建一个使用push_front的迭代器 |
inserter | 创建一个使用insert的迭代器,此函数元素被插入到给定迭代器表示的元素之前 |
list<int> lst = {1,2,3,4};
copy(lst.begin(),lst.end(),front_inserter(lst1)); //lst1 = 4 3 2 1
copy(lst.begin(),lst.end(),inserter(lst2,lst2.begin())); // lst2 = 1 2 3 4
4.2、iostream迭代器
istream_iterator迭代器读取输入流,ostream_iterator迭代器向一个输出流写数据。
流迭代器与流进行绑定,从流对象读取数据以及向其写入数据。
当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。
istream_iterator<int> int_it(cin); //从cin读取int
istream_iterator<int> eof; //尾后迭代器
eof被定义为空的istream_iterator,从而可以当作尾后迭代器来使用。
我们可以使用istream迭代器来构造序列。
istream_iterator<int> in(cin),eof;
vector<int> vec(in,eof); //从迭代器范围构造vec
istream_iterator<T> in(is); | in从输入流is读取类型尾T的值 |
in1 == in2 in1 != in2 |
in1和in2必须读取相同类型。 若它们都是尾后迭代器或绑定相同的输入,则相等 |
*in | 返回从流中读取的值 |
in->mem | 与(*in).mem的含义一致 |
++in,in++ |
使用元素类型所定义的>>运算符从流中读取值 前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值 |
标准库并不保证迭代器立即从流读取数据,具体实现直到我们使用迭代器时才读取。
创建ostream_iterator时,可以提供第二参数,该参数是字符串,在输出每个元素后都会打印。
ostream_iterator<T> out(os) | out将类型为T的值写到输出流os中 |
ostream_iterator<T> out(os,d) |
每个值后面都输出一个d |
out = val |
用<<运算符写入到out所绑定的ostream中 val的类型必须与out可写的类型兼容 |
*out,++out,out-- | 这些运算符是存在,但不对out做任何事情 |
4.3、反向迭代器
反向迭代器是在容器中从尾元素向首元素反向移动的迭代器。
除了forward_llist之外,其他容器都支持反向迭代器。
反向迭代器转换成普通迭代器时,需要调用reverse_iterator的base成员函数来完成操作。
元素范围对称,正向与反向迭代器位置元素相等。
5.泛型算法结构
输入迭代器 | 只读,不写;单遍扫描,只能递增 |
输出迭代器 | 只写,不读;单遍扫描,只能递增 |
前向迭代器 | 可读写;多遍扫描,只能递增 |
双向迭代器 | 可读写;多遍扫描,可递增递减 |
随机访问迭代器 | 可读写;多遍扫描,支持全部迭代器运算 |
5.1、类迭代器
一个高层类别的迭代器支持底层类别迭代器的所有操作。
输入迭代器只用于顺序访问,解引用、递增递减操作是有效的。
输出迭代器只能被赋值一次,只能用于单遍扫描算法。
前向迭代器只能在序列中沿着一个方向移动,可多次读写同一元素。
随机访问迭代器提供在常量时间内访问序列中任意元素的能力。
5.2、算法形参模式
(beg,end,other_args) |
(beg,end,dest,other_args) |
(beg,end,beg2,other_args) |
(beg,end,beg2,end2,other_args) |
beg2默认比beg1长度更长或相等。
dest参数是一个表示算法可以写入的目的位置的迭代器,假定不管写入多少个元素都是安全的。
5.3、算法命名规范
接受谓词参数来代替<或==运算符的算法,以及那些不接受额外参数的算法,通常是重载的函数。
unique(beg,end);
unique(beg,end,comp);
_if版本的算法用于使用过一元谓词的函数用名。
find(beg,end);
find_if(beg,end,pred);
unique和find算法提供了命名上差异的版本,避免产生重载歧义。
写到额外目的空间的算法都在名字后面附加一个_copy。
6.特定容器算法
与其他容器不同,list和forward_list定义了专属的成员函数算法。
lst.merge(lst2) lst.merge(lst2,comp) |
将来自lst2的元素合并入lst;lst和lst2都必须是有序的。 在合并之后,lst2变为空。 |
lst.remove(val) lst.remove_if(pred) | 调用erase删除掉与给定值相等或令一元谓词为真的元素 |
lst.reverse() |
反转lst中元素的操作 |
lst.sort() lst.sort(comp) | 使用<或给定比较操作排序元素 |
lst.unique() lst.unique(pred) | 调用erase删除同一个值的拷贝 |
链表类型定义了spllice算法,该算法是链表结构所特有的。
splice算法有两种形式:
lst.splice(args); //将args中元素移动到目的位置前
lst1.splice_after(args); //移动到目的位置后
(p,lst2) |
p是一个指向lst中的元素或lst1的首前位置的迭代器。 函数将lst2的所有元素移动到p之前或lst2之后,lst2元素全部删除。 |
(p,lst2,p2) |
p2指向lst2,将p2指向的元素移动到lst中。 将p2之后的元素移动到lst1中。 lst2可以是与lst或lst1相同的链表。 |
(p,lst2,b,e) |
b,e表示lst2中的合法范围,将给定范围中的元素从lst2移动到lst或lst1。 |
链表版本的算法会改变底层的容器,删除list中的元素(其他版本的算法只是覆盖)。