《C++ Primer》第十章 泛型算法
10.1 概述
大多数算法定义在头文件algorithm中,还有一些算法在numeric中。例如标准库算法find:
int val = 42;//即将查找的值
//如果在vec中找到想要的元素,则返回结果指向它,否则返回vec.cend()
auto result = find(vec.cbegin(), vec.end(), val);
string val = "a value";//我们要查找的值
//此调用在list中查找string元素
auto result=find(lst.cbegin(),lse.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[2]和ia[3]中查找给定元素
auto result = find(ia+1,ia+4,val);
10.2 初识泛型算法
只读算法:find()、count()、accumulate()
accumulate(): 定义在numeric中,接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。例如:
//对vec中的元素求和,和的初值是0
int sum = accumulate(vec.cbegin(), vec.cend(), 0);
//通过调用accumulate将vector中所有string元素连接起来
string num = accumulate(v.cbegin(), v.cend(), string(""));
//将空串当做一个字符串字面值传递给第三个参数是不可以的,会导致一个编译错误
//错误:const char* 上没有定义+运算符
string sum = accumulate(v.cbegin(), v.cend(), "");
只读算法equal(): 用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素和第二个序列中的对应元素进行比较。如果所有对应元素都相等,则返回true, 否则返回false。此算法接受三个迭代器:前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素。
//roster2中的元素数目应该至少与roster1一样多
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
写容器元素的算法:fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill将给定的这个值赋予输入序列中的每个元素。
fill(vec.begin(), vec.end(), 0);//将每个元素重置为0
//将容器中的一个子序列设置为10
fill(vec.begin(), vec.begin() + vec.size()/2, 10);
算法不检查写操作:
fill_n接受一个单迭代器、一个计数器和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素。
vector<int> vec;
//使用vec, 赋予不同值
fill_n(vec.begin(), vec.size(), 0);//将所有元素重置为0
fill_n(dest, n ,val)
fill_n假定dest指向一个元素,而从dest开始的序列至少包含n个元素
vector<int> vec;//容量
//灾难:修改vec中的10个(不存在)的元素
fill_n(vec.begin(), 10, 0);
back_inserter:接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。
vector<int> vec;//空向量
auto it = back_inserter(vec);//通过它赋值会将元素添加到vec中
*it = 42;//vec现在有一个元素,值为42
//使用back_inserter来创建一个迭代器,作为算法的目的位置来使用
//正确:back_inserter创建一个插入迭代器,可以用来向vec添加元素
fill_n(back_inserter(vec), 10 , 0);
拷贝算法:copy可实现内置数组的拷贝
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
算法replace:读入一个序列,并将其中所有给定值的元素都改为另一个值。接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值。
//将所有值为0的元素改为42
replace(ilist.begin(), ilist.end(), 0, 42);
//保持原序列不变,使用back_inserter按需要增长目标序列
replace_copy(ilst.cbegin(), ilist.cend(),
back_inserter(ivec), 0, 42);
重排容器元素的算法:
消除重复单词:
void elimDups(vector<string> &words)
{
//按字典序排序words,以便查找重复单词
sort(words.begin(), words.end());
//unique重排输入范围,使得每个单词只出现一次
//排列在范围的前部, 返回指向不重复区域之后一个位置的迭代器
auto end_unique = unique(words.begin(), words.end());
//使用向量操作erase删除重复单词
words.erase(end_unique, words.end());
}
10.3定制操作
谓词:返回结果是一个能用作条件的值。分为一元谓词和二元谓词。一元、二元代表着接受几个参数。示例如下:
//比较函数,用来按长度排序单词
bool isShorter(const string &s1, const string &s2)
{
return s1.size()<s2.size();
}
//按长度由短至长排序words
sort(words.begin(), words.end(), isShorter);
lambda表达式:表示一个可调用的代码单元。可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。表达式形式如下:
[capture list] (parameter list) -> return type { function body }
其中,capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空):return type、parameter list 和 function body 与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,lambda必须使用尾置返回。
isShorter函数的lambda:
[](const string &a, const string &b){
return a.size()<b.size();
}
//使用lambda来调用stable_sort
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{return a.size() < b.size();});
使用捕获列表:
[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; });]
//计算满足size>=sz的元素的数目
auto count = words.end() - wc;
cout << count <<" "<<make_plural(count, "word", "s")
<< " of length "<< sz << " or longer"<<endl;
for_each算法: 接受一个可调用对象,并对输入序列中每个元素调用此对象。
//打印长度大于等于给定值的单词,每个单词后面接一个空格
for_each(wc, words.end(),
[](const string &s){cout<<s<<" "});
cout<<endl;
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保存了我们创建它时的拷贝
}
引用捕获:
void fcn2()
{
size_t v1 = 42;//局部变量
//对象f2包含v1的引用
auto f2 = [&v1]{ return v1; };
v1 = 0;
auto j = f2();//j为0;f2保存v1的引用,而非拷贝
}
希望biggies函数接受一个ostream的引用,用来输出数据,并接受一个字符作为分隔符
void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout,
char c = ' ')
{
...
//打印count的语句改为打印os
for_each(words.begin(), words.end(),
[&os, c](const string &s) { os<<s<<c; });
}
隐式捕获:为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用捕获引用方式, =则表示采用值捕获方式。
//sz为隐式捕获,值捕获方式
wc = find_if(words.begin(), words.end(),
[=](const string &s)
{ return s.size()>=sz; });
可以混合使用隐式捕获和显示捕获:
void biggies(vector<string> &words,
vector<string>:: size_type sz,
ostream &os = cout, char c=' ')
{
//os隐式捕获,引用捕获方式;c显示捕获,值捕获方式
for_each(words.begin(), words.end(),
[&, c](const string &s) { os<<s<<c; });
for_each(words.begin(),words.end(),
[=,&os](const string &s) { os<<s<<c; });
}
可变lambda:默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。可以在参数列表首加上mutable来改变被捕获的变量值。:
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
}
指定lambda返回类型:当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型
transform(vi.begin(), vi.end(), vi.begin(),
[](int i)->int
{ if(i<0) return -i; else return i; });
参数绑定:在find_if调用中的lambda比较一个string 和一个给定大小。但不能用这个函数作为find_if的一个参数。因为find_if接受一元谓词,因此传递给find_if的可调用对象必须接受单一参数。
bool check_size(const string &s, string::size_type sz)
{
return s.size()>=sz;
}
标准库bind函数:将bind函数看作一个通用的函数适配器,它接受一个可调用对象。生成一个新的可调用对象来“适应”原对象的参数列表:
auto newCallable = bind(callable, arg_list);
绑定check_size的sz参数:
//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);
//使用bind,可以将原来基于lambda的find_if调用
auto wc = find_if(words.begin(), words.end(), [sz](const string &a));
//替换为check_size版本
auto wc = find_if(words.begin(), words.end(),bind(check_size, _1, size));
使用placeholders名字:如_1对应的using声明为:
using std:: placeholders::1;
可以另一种不同形式的using语句,希望namespace_name的名字都可以可以直接使用:
using namespace namespace_name;
用bind重排参数顺序:
//按单词长度由短至长排序
sort(words.begin(), words.end(), isShorter);
//按单词长度由长至短排序
sort(words.begin(), words.end(), bind(isShorter, _2, _1));
绑定引用参数:与lambda相似,有时对有些绑定的参数希望以引用方式传递,或是要绑定参数的 类型无法拷贝。例如,为了替换一个引用方式捕获ostream的lambda:
//os是一个局部变量,引用是一个输出流
//c是一个局部变量,类型为char
for_each(words.begin(),words.end(),[&os, c](const string &s){ os<<s<<c; });
//可以很容易编写一个函数,完成相同的工作
ostream &print(ostream &os, const string &s, char c)
{
return os<<s<<c;
}
但是不能直接用bind来代替对os的捕获:
//错误:不能拷贝os
for_each(words.begin(), words.end(), bind(print,os,_1,' '));
原因在于bind拷贝其参数,而我们不能拷贝一个ostream。如果希望传递给bind一个对象而又不拷贝它,就必须使用标准库ref函数:
for_each(words.begin(),words.end(),bind(print, ref(os), _1,' '));
函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个cref函数,生成一个保存const引用的类。
10.4 再探迭代器
插入迭代器:被绑定到一个容器上,用来向容器插入元素。
back_inserter: 调用push_back,总是插入到容器尾元素之后
front_inserter:调用push_front,总是插入到容器元素之前
inserter: 调用insert,总是插入到给定位置
list<int> lst = {1,2,3,4};
list<int> lst2,lst3;//空lst
//拷贝完成后,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()));
流迭代器:被绑定到输入或输出流上,可用来遍历所有关联的IO流
iostream迭代器:istream_iterator读取输入流,ostream_iterator向一个输出流写数据。
反向迭代器:向后而不是向前移动。除了forward_list之外的标准库容器都有反向迭代器。
反向迭代器需要递减运算符。
移动迭代器:不是拷贝其中的元素,而是移动它们。
10.5 泛型算法结构
算法形式参数:
10.6 特定容器算法
splice成员: