10.1 概述
- 泛型算法(通用算法):可用于不同类型的容器和不同类型的元素。
- 容器中定义的操作非常有限,其它操作(例如:查找特定元素、替换或删除一个特定元素、排序等)都是通过一组泛型算法来实现的。
- 算法通过在迭代器上进行操作来实现类型无关。
- 大多数算法都定义在头文件algorithm中。标准库还在头文件numeric中定义了一组数值泛型算法。
- 迭代器令算法不依赖于容器,但算法依赖于元素类型的操作(大多数算法都使用了一个或多个元素类型上的操作)。
- 算法永远不会执行容器的操作,例如改变底层容器的大小。
int val = 42; //将查找的值
//如果在vec中找到想要的元素,则返回结果指向它,
//否则返回结果为vec.cend(),即第二个参数
auto result = find(vec.cbegin(), vec.cend(), val);
//报告结果
cout<<“值:”<<val<<(result == vec.cend()?“ 不存在”:“ 存在 ")<<endl;
string val = "a value"; //我们要查找的值
//此调用在lists中查找string元素
auto result = find(list.cbegin(), list.cend(), val);
//由于指针就像内置数组上的迭代器一样,所以也可以用find在数组中查找
int ia[] = {27,210,12,47,109,83};
int val =83;
int *result = find(begin(ia),end(ia),val);
//在从ia[1]开始,直至(但不包含)ia[4]的范围内查找元素
auto result = find(ia+1,ia+4,val);
10.2 初识泛型算法
- 理解算法最基本的方法:了解它们是否读取元素、改变元素或是重排元素顺序。
10.2.1 只读算法
- 只读算法:只会读取其输入范围内的元素,而不改变元素,如find、accumulate、equal。
- accumulate的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型,序列中元素类型必须与第三个参数匹配或者能转换为第三个参数的类型。
//对vec中的元素求和,和的初值为0(第三个参数还决定了返回类型,以及+的使用)
int sum = accumulate(vec.cbegin(), vec.cend(),0);
string sum = accumulate(v.cbegin(),v.cend(),string(""));//正确
string sum = accumulate(v.cbegin(),v.cend(),"");//错误:const char* 没有定义+运算符
- 对于只读取而不改变元素的算法,通常最好使用cbegin()和cend()。但如果计划使用算法返回的迭代器来改变元素的值,就需要使用begin()和end()的结果作为参数。
- equal用于确定两个序列是否保存相同的值。
- 那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
//第三个参数表示第二个序列的首元素
//roster2中的元素数目应该至少与roster1一样多
equal(roster1.cbegin(),roster1.cend(),roster2.cbegin());
//上面的roster1可以是vector<string>
//而roster2是list<const char*>只要能够访问,能够比较即可
10.2.2 写容器元素的算法
- 算法不检查写操作。
fill(vec.begin(),vec.end(),0);//将每个元素重置为0
//将容器的一个子序列设置为10
fill(vec.begin(),vec.begin() + vec.size/2,10);
vector<int> vec; //空vector
//使用vec,赋予它不同值
fill_n(vec.begin(),vec.size() ,0); //将所有元素重置为0
fill_n(dest,n,val); //dest指向一个元素,而从dest开始至少需要包含n个元素
//下面的代码是错误的
vector<int> vec; //空向量
//灾难:修改vec中的10个(不存在)元素
fill_n(vec.begin(),10,0);
- 向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。
- 一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器。如用算法向容器写入数据,使用back_inserter。back_inserter是定义在头文件iterator中的一个函数:接受指向容器的引用,返回绑定该容器的插入迭代器。常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。
vector<int> vec; //空向量
auto it = back_inserter(vec); //通过it赋值会将元素添加到vec中
*it = 42; //vec中现在有一个元素,值为42
vector<int> vec2;
//正确:back_inserter创建一个插入迭代器,可用来向vec添加元素
fill_n(back_inserter(vec2),10,0);//添加10个元素到vec2
//每次赋值,会在迭代器上调用push_back
int a1[]={0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)]; //a2与a1大小一样
//ret指向拷贝到a2的尾元素之后的位置
auto ret = copy(begin(a1),end(a1),a2);//把a1的内容拷贝给a2
//将所有值为0的元素改为42
replace(list.begin(),list.end(),0,42);
//使用back_insterter按需要增长目标序列
replace_copy(ilist.cbegin(),ilist.cend(),back_inserter(ivec),0,42);
//上面的语句调用后,ilist并未改变,ivec包含ilst的一份拷贝
//不过原来在ilist中值为0的元素在ivec中都变成了42
10.2.3 重排容器元素的算法
//消除重复单词
void elimDups(vector<string>& words)
{
//按字典顺序排序words,以便查找重复单词
sort(words.begin(), words.end());
//unique消除相邻的重复项
//排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());
}
10.3 定制操作
10.3.1 向算法传递函数
- 谓词:是一个可调用的表达式,其返回结果是一个能用作条件的值。
一元谓词:只接受单一参数。
二元谓词:有两个参数。
//比较函数,用来按长度排序单词
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
// sort默认使用元素类型的<运算符
//按长度由短至长排序words,sort可以接受一个二元谓词参数
sort(words.begin(),words.end(),isShorter);
elimDups(words);
//stable_sort:稳定排序算法,维持相等元素的原有顺序
stable_sort(words.begin(), words.end(), isShorter);
for ( auto &word : words) {//无需拷贝字符串
cout << word <<" ";
}
10.3.2 lambda表达式
- 对于一个对象或一个表达式,如果可以对其使用调用运算符则称它为可调用的。
- 一个lambda表达式表示一个可调用的代码单元,可将其理解为一个未命名的内联函数。(C++11)
// 局部变量列表 可以忽略参数列表和返回类型
[capture list] (parameter list) -> return type {function body}
- 如果lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void。
- lambda不能有默认参数。
- 空捕获列表表明lambda不使用它所在函数中的任何局部变量。
- 一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
- 捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字。
10.3.3 lambda捕获和返回
- 当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型和该类型的一个对象。
- 默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。
- 采用值捕获的前提是变量可以拷贝,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝,随后对其修改不会影响到lambda内对应的值。
// 值捕获
void fcn1(){
size_t v1 = 42; // 局部变量
// 将v1拷贝到名为f的可调用对象
auto f = [v1] { return v1; };
v1 = 0;
auto j = f(); // j 为 42; f stored a copy of v1 when we created it
}
- 当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。
// 引用捕获
void fcn2(){
size_t v1 = 42; // local variable
// the object f2 contains a reference to v1
auto f2 = [&v1] { return v1; };
v1 = 0;
auto j = f2(); // j is 0; f2 refers to v1; it doesn't store it
}
- 建议:尽量保持lambda的变量捕获简单化,如果可能的话,应该避免捕获指针或引用。
- 隐式捕获:让编译器根据lambda体中的代码来推断需要使用哪些变量,在捕获列表中写&(引用捕获)或=(值捕获)。
- 当混合使用隐式和显式捕获时,捕获列表中的第一个元素必须是&或=,而且显式捕获的变量必须使用与隐式捕获不同的方式。
//sz为隐式捕获,值捕获方式
wc = find_if(words.begin(),words.end(),[=](const string &s){return s.size() >= sz;});
//可以混合使用隐式捕获和显示捕获
void biggies(vector<string> &words,vecotr<string>::size_type sz,
ostream &os = cout, char c=' ')
{
//其他处理与前例一样
//偶数隐式捕获,引用捕获方式;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。因此,可变lambda能省略参数列表。
void fcn3(){
size_t v1 = 42; // local variable
// 对于值拷贝的变量,如果需要修改,必须加上关键字mutable
auto f = [v1]() mutable { return ++v1; };
v1 = 0;
auto j = f(); // j is 43
}
- 一个引用捕获的变量是否可以修改依赖于此引用指向的是否是一个const类型。
void fcn4(){
size_t v1 = 42; // local variable
// 对于非const变量的引用,可以通过f2中的引用修改
auto f2 = [&v1] { return ++v1; };
v1 = 0;
auto j = f2(); // j is 1
}
- lambda必须使用尾置返回来指定返回类型。
//错误:无法推断return类型
transform(vi.begin(), vi.end(), vi.begin(),[](int i){ if (i<0) return –i; else return i;};
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int
{ if (i<0) return –i; else return i;};
10.3.4 参数绑定
- bind(C++11):接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
- bind定义在functional头文件中,调用bind的一般形式为:
auto newCallable = bind(callable, arg_list);
using namespace std;
using namespace std::placeholders;
vector<string> words = { "string1","abcde" };
bool check_size(const string& s, string::size_type sz)
{
return s.size() >= sz;
}
int main()
{
//check6是一个可调用对象,接受一个string类型的参数
//并用此string和值6来调用check_size
auto check6 = bind(check_size, _1, 6);
string s = "hello";
bool b1 = check6(s);//check6(s)会调用check_size(s,6);
auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, 6));
auto wc2 = find_if(words.begin(), words.end(), check6);
}
- 默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。
//g是一个有两个参数的可调用对象
auto g = bind(f,a,b,_2,c,_1);
//g(X,Y)的调用会映射到:f(a,b,Y,c,X)
//按单词长度由短至长排序
sort(words.begin(),words.end(),isShorter);
//按单词长度由长至短排序
sort(words.begin(),words.end(),bind(isShorter,_2,_1));
//当sort需要比较两个元素A和B时,调用isShorter(A,B)
//当sort比较两个元素时,就好像调用了isShorter(B,A)一样
- 如果希望传递给bind一个对象而又不拷贝它,就必须使用ref函数。ref函数返回一个对象,包含给定的引用,此对象是可以拷贝的。
//错误:不能拷贝os
for_each(words.begin(),words.end(),bind(print,os,_1,' '));
//对于ostream对象,不能拷贝。必须使用标准库ref函数包含给定的引用
for_each(words.begin(),words.end(),bind(print,ref(os),_1,' '));
10.4 再探迭代器
10.4.1 插入迭代器
- 插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。
- 插入迭代器有三种类型:
a. back_inserter,创建一个使用push_back的迭代器。
b. front_inserter,创建一个使用push_front的迭代器。
c. inserter,创建一个使用insert的迭代器,插入指定迭代器之前的位置。
//it是由inserter生成的迭代器
*it = val;//其效果与下面代码一样
it=c.insert(iter,val); //it指向新插入的元素
++it;//递增it使它指向原来的元素
- front_inserter生成的迭代器总是指向容器的第一个元素。这点与inserter生成的迭代器不同。
list<int> lst = {1,2,3,4};
list<int> lst2,lst3;//空list
//拷贝完成后,lst2包含4,3,2,1
copy(lst.cbegin(),lst.cend(),front_inserter(lst2));
//拷贝完成之后,lst3包含1 2 3 4
copy(lst.cbegin(),lst.cend(),inserter(lst3,lst3.begin()));
10.4.2 iostream迭代器
- 将对应的流,当做一个特定类型的元素序列来处理。
- 使用流迭代器,可以用泛型算法从流对象读取数据以及向其写入数据。
- 对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。
istream_iterator<int> int_iter(cin); //绑定一个流,从cin读取int
istream_iterator<int> eof;//默认初始化迭代器,尾后迭代器
while(in_iter != eof)
//解引用迭代器,获得从流读取的前一个值
vec.push_back(*in_inter++);//后置递增运算读取流,返回迭代器的旧值
//可以将程序重写为下面的形式
istream_iterator<int> in_iter(cin),eof;
vector<int> vec(in_iter,eof);//从迭代器范围构造vec
ifstream in("afile");
istream_iterator<stirng> str_it(in); //从“afile”读取字符串
- 使用算法操作流迭代器。
istream_iterator<int> in(cin),eof;
cout<<accumulate(in,eof,0)<<endl;
//此调用会计算出从标准输入读取的值的和。
//如果输入为1 2 5,则输出为8
//使用ostream_iterator来输出值的序列:
ostream_iterator<int> out_iter(cout, " ");
for(auto e:vec)
*out_iter++=e;//赋值语句实际上将元素写到cout
cout<<endl;
//项out_iter赋值时,可以忽略解引用和递增
for(auto e:vec)
out_iter = e;//赋值语句将元素写到cout
cout<endl;
//通过copy来打印
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");
//将第一笔交易记录存在sum中,并读取下一条记录
Sales_item sum = *item_iter++;
while(item_iter != eof){
//如果当前交易记录(存在item_iterm中)有着相同的ISBN号
if(item_iter->isbn() == sum.isbn())
sum+=*item_iter++; //将其加到sum上并读取下一条记录
else{
out_iter = sum; //
sum = *item_iter++; //读取下一条记录
}
}
out_iter = sum; //记得打印最后一组记录的和
10.4.3 反向迭代器
- 反向迭代器需要递减运算符,所以forward_list或流迭代器不能创建反向迭代器。
vector<int> vec = { 0,1,2,3,4,5,6,7,8,9 };
//从尾元素到首元素的反向迭代器
for (auto r_iter = vec.crbegin(); r_iter != vec.crend(); ++r_iter)
cout << *r_iter << endl;//打印9,8,7,...,0
sort(vec.begin(), vec.end());//按“正常序”排序vec,升序
sort(vec.rbegin(), vec.rend());//按逆序排序:将最小元素放在vec的末尾,降序
string line = "FIRST,MIDDLE,LAST";
//在一个逗号分隔的列表中查找第一个元素
auto comma = find(line.cbegin(), line.cend(), ',');
cout << string(line.cbegin(), comma) << endl;
//在一个逗号分隔的列表中查找最后一个元素
auto rcomma = find(line.crbegin(), line.crend(), ',');
//错误:将逆序输出单词的字符
cout << string(line.crbegin(), rcomma) << endl;
//正确:得到一个正向迭代器,从逗号开始读取字符直到line末尾
cout << string(rcomma.base(), line.cend()) << endl;
10.5 泛型算法结构
10.5.1 5类迭代器
- 迭代器类别(根据支持的操作不同)
输入迭代器:可以读取序列中的元素。只用于顺序访问。必须支持:
a. 用于比较两个迭代器是否相等的运算符(==,!=)
b. 用于推进迭代器的前置和后置递增运算符(++)
c. 用于读取元素的解引用运算符(*);出现在赋值运算符右侧
d. 箭头运算符(->)
输出迭代器:可以看作输入迭代器功能上的补集—只写而不读元素。只能向一个输出迭代器赋值一次。只能用于单遍扫描算法。用作目的位置的迭代器通常都是输出迭代器。
前向迭代器:可以读写元素。只能在序列中沿一个方向移动。支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。
双向迭代器:可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,还支持前置和后置递减运算符。
随机访问迭代器:提供在常量时间内访问序列中任意元素的能力。支持双向迭代器的所有功能。
- 类似容器,迭代器的操作也分层次。高层类别的迭代器支持所有底层类别迭代器的操作,如:
a. ostream_iterator只支持递增、解引用(出现在赋值运算符左侧)和赋值。
b. vector、string和deque的迭代器还支持递减、关系和算术运算。 - C++标准指明了泛型和数值算法的每个迭代器参数的最小类别。对每个迭代器参数来说,其能力必须与规定的最小类别至少相当。如:replace_copy的前两个迭代器至少是前向迭代器。第三个至少是输出迭代器。
10.5.2 算法形参模式
- 向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据。
- 大多数算法具有如下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);
- 接受单独beg2的算法假定从beg2开始的序列与beg和end所表示的范围至少一样大。
10.5.3 算法命名规范
- 规定如何提供一个操作替代默认的运算符,以及算法将输出数据写入输入序列还是一个分离的目的地等问题。
- 接受谓词参数来代替<或==运算符的算法,以及那些不接受额外参数的算法,通常都是重载的函数。
- 接受一个元素值的算法通常有另一个不同名的版本,该版本接受一个谓词代替元素值。接受谓词参数的算法都有附加的_if前缀。
//将相邻重复元素删除
// 重载
unique(beg,end); //使用 == 运算符比较元素
unique(beg,end,comp); //使用comp比较元素
//在范围内查找特定元素第一次出现的位置。
// 通过名字区分
find(beg,end,val);
find_if(beg,end,pred); //查找第一个令pred为true的元素
reverse(beg,end);//翻转输入范围中元素的顺序 不拷贝
reverse_copy(beg,end,dest); //将元素逆序拷贝到dest 拷贝
//从vi中删除奇数元素 不拷贝
remove_if(v1.begin(),v1.end(),[](int i){return i%2;});
//将偶数元素从v1拷贝到v2;v1不变 拷贝
remove_copy_if(v1.begin(),v1.end(),back_inserter(v2),
[](int i){return i%2;});
10.6 特定容器算法
- 一个链表可以通过改变元素间的链接而不是真的交换它们的值来快速“交换”元素。
- 对于list和forward_list,应该优先使用成员函数版本的算法而不是通用算法。
- 链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器。
- splice算法是链表结构所特有的。