C++ Primer笔记10---chapter10 泛型算法
Posted Ston.V
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ Primer笔记10---chapter10 泛型算法相关的知识,希望对你有一定的参考价值。
1.泛型算法:
可用于不同类型的元素和多种容器类型。往往是迭代器范围作为泛型算法的参数,迭代器使得算法不依赖于容器,泛型算法本身不会执行容器操作,而只会运作于迭代器之上,执行迭代器的操作(但是算法依赖于元素类型的操作,比如判断相等,相加,对于自定义类型我们往往可能需要重载运算符),那么也就导致泛型算法不会改变底层容器的大小,可能会改变、移动元素但永远不会直接添加、删除元素。
2.使用写算法时,需要注意算法本身不会改变容器大小,他本身多大,泛型算法操作后还是多大。
vector<int> vec; //灾难:修改vec中10个不存在的元素 fill_n(vec.begin(),10,0); //正确:back_inserter会返回一个迭代器,给这个迭代器赋值就相当于push_back一个元素,每次迭代都会有新的迭代器返回,进而使用它来插入元素,最终插入了10个0 fill_n(back_inserter(vec),10,0)
3.举一个例子:将vector中的元素排序,并消除重复元素
注意泛型算法的函数unique无法改变容器大小,因此最后还是需要使用容器算法erase来删除重复元素
void elimDups(vector<string> &words){ //排序 sort(words.begin(),words.end()); //unique去重,重复出现的单词会被统一放在words的最后面,返回指向重复区域的迭代器 auto end_unique = unique(words.begin(),words.end()); //使用向量操作erase删除重复单词 words.erase(end_unique,words.end()); }
4.lambda表达式:
目前我们学过的几种可调用对象:函数,函数指针,重载了函数运算符的类,lambda表达式
一个lambda表达式表示一个可调用的代码单元,可以理解为未命名的内联函数
//其中捕获列表(通常为空)和函数体不能为空 //捕获列表只用于局部非static变量,lambda可以直接是用局部static变量和在他所在函数之外声明的名字 [capture list](param list) -> return type{func body}
//将vector中的单词去重并排序,在vector中,找到所有比给定长度更长的单词并打印出来 void biggies(vector<string> &words,vector<string>::size_type sz){ //上文的函数,排序且去重 elimDups(words); //稳定的排序,按照长短排序的同时保证相同长度的仍按字典序排列 stable_sort(words.begin(),words.end(), [](const string &a,const string &b) {return a.size()<b.size();}); //调用find_if找到第一个指向size()>sz=的元素,find_if会返回第一个func返回true的元素的迭代器 auto wc = find_if(words.begin(),words.end(), [sz](const string &a){return a.size()>=sz;}); //计算满足size>=sz的元素个数 auto cnt=words.end()-wc; //打印长度大于等于给定值的单词,每个单词后面跟一个空格 for_each(wc,words.end(), [](const string &s){cout<<s<<" ";}); cout<<endl; }
5.lambda使用:
5.1 lambda对于非static局部变量捕获:
1)可以使用[=]或者[&]来表示为值捕获或者引用捕获
2)如果是混合捕获,需要[=,&a]或者[&,a],第一个符号表示默认的捕获方式,剩下的为与默认捕获方式不同的变量
5.2 可变lambda:
对于值被拷贝的变量,lambda不会改变其值,如果希望改变一个被值捕获变量的值,需要在参数列表首加上关键字mutable,此时可以省略参数列表
void fun3(){ size_t v1=42; //值捕获,此时lambda并没有被调用,到后面执行f()时,才给拷贝来的42执行自增运算 auto f=[v1]() mutable {return ++v1;}; v1=0; auto j=f(); //j=43 cout<<j<<endl; } void fun4(){ size_t v1=42; //引用捕获,此时lambda并没有被调用,到后面执行f()时,才给v1的引用执行自增运算 auto f=[&v1]() mutable {return ++v1;}; v1=0; auto j=f(); //j=1 cout<<j<<endl; }
5.3 lambda返回值:
如果一个lambda或含了return之外的任何语句,编译器假定此lambda返回void,被推断为返回void的lambda不能返回值(自己没有指定返回类型,函数体只有一句return时,才能够有返回值)
5.4参数绑定
对于只在一两个地方使用的简单操作,lambda表达式最有用;如果要在很多地方使用,那么就该定义为函数;如果lambda捕获列表为空,通常可以使用函数来直接代替,但对于捕获局部变量的lambda,用函数来替换他就不那么容易。
例如,对于find_if(如前文,需要找出vector<string>中比给定值长的string对象),他只接受一元谓词,我们使用函数来写需要给这个函数两个参数(一个是被对比的string,一个是指定的长度),那么函数只能做成二元谓词的可调用对象,也就无法作为find_if的参数;但是使用lambda可以将这个给定值作为捕获列表而不作为此参数,参数只有待对比的string对象,这样就称为了一元谓词的可调用对象。
//前文中的code,find_if接受了一个一元谓词的可调用对象(lambda实现,sz放进了捕获列表) auto wc = find_if(words.begin(),words.end(), [sz](const string &a){return a.size()>=sz;}); //如果使用函数check_size实现,函数只能是二元谓词,不可作为find_if的参数 bool check_size(const string &s,string::size_type sz) { return s.size() >= sz; }
但是,我们也可使不使用lambda处理,使用bind函数来对参数进行处理
bind函数作用:
1)修正参数的个数//给一个调用对象,返回新的调用对象,其中arg_list表示参数列表 //arg_list中可能有形如_1 , _2的占位符,这表示新的可调用对象newCallable的第一个和第二个参数;占位符在arg_list中的位置也就是真正调用的函数Callable时这些参数的位置 auto newCallable = bind(Callable , arg_list); //其中check6值接受一个参数(用_1表示),最终实际会调用check_size(_1,6),而6就是size //这样二元谓词就转化为一元谓词 auto check6 = bind(check_size, _1 ,6); //使用bind来返回一个新的可调用对象(返回的还是函数,其中_1表示传给新的可调用对象的第一个参数,实际上也就这一个参数) auto wc = find_if(words.begin(),words.end(), bind(check_size, _1 , sz));
序对于占位符,注意使用命名空间using namespace placeholders,此命名空间定义在functional头文件中
2)绑定给定操作对象中的参数或者重新安排其顺序
绑定参数(绑定非占位符的参数,类似默认形参)://f接受5个参数,g只接受2个参数 auto g= bind(f,a,b,_2,c,_1); g(X,Y); //g(X,Y)实际会调用 f(a,b,Y,c,X);
还可以使用bind来重排参数顺序:
bool isShorter(const string &a,const string &b){ return a.size()<b.size(); } //按单词长度由短至长排序 sort(words.begin(),words.end(),isShorter); //按单词长度由长至短排序,牛批!!!! sort(words.begin(),words.end(),bind(isShorter,_2,_1));
绑定引用参数:
在上面函数映射的过程中,对于bind中不是占位符的参数被拷贝到返回的新的可调用对象中去,但是对于些绑定的参数可能希望以引用的方式传递,或者要绑定的类型无法拷贝(流对象),此时如果希望传递给bind一个对象而又不拷贝它,必须使用函数ref/cref(定义在functional头文件中)
//输出vector中的string对象到流中,以空格间隔 for_each(words.begin(),words.end(), [&os,c](const string &s){ os<<s<<c;}) //可以编写一个函数打倒上文中lambda的作用 os &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, ' ')); //正确:使用ref for_each(words.begin(),words.end(), bind(print, ref(os), _1, ' '));
6.四种迭代器(头文件iterator中的)
插入迭代器 back_inserter、front_inserter、inserter 接受参数为容器(inserter还需要再指定迭代器位置),返回一个迭代器it,给*it赋值即可实现插入元素 流迭代器 istream_iterator、ostream_iterator istream_iterator<T> in(is); in从流is读取类型为T的值
istream_iterator<T> end;定义一个空迭代器,表示尾后迭代器
不支持递减操作
反向迭代器 vec.crbegin(首前)、vec.crend(尾) ++it表示前一个元素,--it表示后一个元素 移动迭代器 流迭代器使用列子:无非就是从istream读值,想ostream写值,一个求任意数值和的例子
#include <iostream> #include <iterator> using namespace std; int main(){ istream_iterator<int> it(cin),eof; ostream_iterator<int> ot(cout," "); int sum = 0; while(it != eof) sum+=*it++; *ot=sum; return 0; }
反向迭代器:可以用算法透明的向前或向后处理容器
//按“正常序”排序,从小到大 sort(vec.begin(),vec.end()); //按逆序排序,将最小元素放在vec末尾 sort(vec.rbegin(),vec.rend());
反向迭代器的转换:可以通过其成员函数base()得到正向迭代器,但是注意我们的迭代器范围是左闭右开,此时得到的正向迭代器指向的实际上是原迭代器所指的下一个元素(这里的下一个,是正向的下一个),类似的,crbegin()【指向最后一个元素】和cend()【指向尾后元素】也是一样
7.泛型算法结构
在任何其他算法分类之上,还有一组参数规范。大多数算法具有如下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);
其中alg是算法的名字,beg和end表示算法所操作的输入范围。几乎所有算法都接受一个输入范围,是否有其他参数依赖于要执行的操作。这里列出了常见的一种——dest、beg2和end2,都是迭代器参数。顾名思义,如果用到了这些迭代器参数,它们分别承担指定目的位置和第二个范围的角色。除了这些迭代器参数,一些算法还接受额外的、非迭代器的特定参数。
dest参数是一个表示算法可以写入的目的位置的迭代器。算法假定:按其需要写入数据,不管写入多少个元素都是安全的。
如果dest是一个直接指向容器的迭代器,那么算法将输出数据写到容器中已存在的元素内。更常见的情况是,dest被绑定到一个插入迭代器或是一个ostream_iterator。插入迭代器会将新元素添加到容器中,因而保证空间是足够的。ostream_iterator会将数据写入一个输出流,同样不管要写多少个元素都没有问题。
对于list和forward_list应该优先使用成员函数,而非泛型算法,他们的效率更高
8.这些知识很繁杂,可以看的出来c++发展中将很多我们可能/已经产生的需求合并进了语言标准,很丰富。但是我觉得语言本身,不应提供这么多策略内容。“机制”和“策略”需要分离,语言本身应该追求易用高效,减少策略相关的标准。就像bind函数,流迭代器,我们明明可以通过函数封装实现,不需要这么大篇幅的介绍。这些“冗余策略”这无疑增大了使用门槛。
以上是关于C++ Primer笔记10---chapter10 泛型算法的主要内容,如果未能解决你的问题,请参考以下文章
C++ Primer笔记16---chapter13 代码实例
C++ Primer笔记15---chapter13 拷贝控制2