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应该优先使用成员函数,而非泛型算法,他们的效率更高

https://www.cnblogs.com/wuchanming/p/3918411.html

8.这些知识很繁杂,可以看的出来c++发展中将很多我们可能/已经产生的需求合并进了语言标准,很丰富。但是我觉得语言本身,不应提供这么多策略内容。“机制”和“策略”需要分离,语言本身应该追求易用高效,减少策略相关的标准。就像bind函数,流迭代器,我们明明可以通过函数封装实现,不需要这么大篇幅的介绍。这些“冗余策略”这无疑增大了使用门槛。

 

以上是关于C++ Primer笔记10---chapter10 泛型算法的主要内容,如果未能解决你的问题,请参考以下文章

C++ Primer笔记16---chapter13 代码实例

C++ Primer笔记15---chapter13 拷贝控制2

C++ Primer笔记15---chapter13 拷贝控制2

C++ C++ Primer 基础知识笔记

C++ Primer Plus读书笔记

C++ Primer学习笔记