c++ primer 第五版 笔记 第十章

第十章 泛型算法

因翻译太耗时了,还是看中文比较快,先做笔记如下

10.1 概述

我们通常希望,在某个容器中查找某个值。如果为每个容器都提供这样的一个成员函数,那是非常繁琐的操作。

此时,c++标准库提供了一组通用函数,来达到这种效果。

例如:

int val = 42;
auto result = find(vec.cbegin(),vec.cend(),val);
string val = "a value";
auto result = find(lst.cbegin(),lst.cend(),val);
int ia[] = {27,210,12,47,109,83};
int val = 83;
int * result = find(begin(ia),end(ia),val);

上面三组程序,都使用了同一个函数find,来查找,容器中给定范围是否有某个值。

这里的find就是泛型算法中的一种算法。

10.2 初识泛型算法

通常将这些泛型算法分为三类:读算法,写算法,排序算法

10.2.1 读算法

读算法:只会读取容器中的元素,不会修改容器的元素,例子如下:

//accumulate,前两个参数表示需要求和的元素的范围,后一个表示和的初始值。

int sum = accumulate(vec.cbegin(),vec.cend(),0);

accumulate的第三个参数的类型,决定了函数调用哪个加法运算符以及返回值的类型

思考下面的写法:

string sum = accumulate(v.cbegin(),v.cend(),"");

上面的写法会报错误,因为第三个参数决定了函数调用哪个加法,因为第三个参数是字符串字面量,类型为char *,他没有可以使用的加法,所以报错,正确的做法如下:

string sum = accumulate(v.cbegin(),v.cend(),string(""));

对于只读取而不改变元素的算法,通常最好使用cbegin()和cend().但是,如果你使用算法返回的迭代器来改变元素的值,就需要使用begin()和end()的结果作为参数

再来个例子:

//equal 比较两个容器中的元素是否相等,如果相等个,返回true,否则返回false

equal(roster1.cbegin(),roster1.cend(),roster2.cbegin());

上面函数能够正确运行的前提是:roster2中的元素不能比rouster1的元素少

10.2.2 写算法

写算法:表示会修改容器元素值的算法,如下面:

//fill使用第三个参数,来赋值容器中的每一个值

fill(vec.begin(),vec.end(),0);
fill(vec.begin(),vec.begin()+vec.size()/2),10);

注意上面的注释,是赋值,而不是重新创建。思考下面的例子:

//fill_n 第二个参数表示需要赋值的元素的多少
vector<int> vec;
fill_n(vec.begin(),10,0);

这种是错误的,因为,我们想给vec中的十个元素,赋值为0,但是vec中没有任何元素,它是一个空的容器。

为了达到赋值的时候,就能够自动创建一个元素,此处引入一种新的迭代器,叫做插入迭代器。给这个迭代器赋值,就相当于给这个插入一个新值。后面将会有详细的插入迭代器的笔记,此处只需要熟悉下面的代码即可

vector<int> vec;
auto it = back_inserter(vect);

*it = 42; //向容器中插入一个元素,值为42
vector<int> vec;
fill_n(back_inserter(vec),10,0);//向容器中插入10个元素

下面再记一个拷贝算法

int a1[] = {0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)] ;
auto ret = copy(begin(a1),end(a1),a2);
//replace ,查找范围内,所有的0,并替换成42
replace(ilst.begin(),ilist.end(),0,42);
//replace_copy,查找范围内的,所有的0,并将其替换成42,替换的结果,放入第三个迭代器指示的位置中
replace_copy(ilst.cbegin(),ilist.cend(),back_inserter(ivec),0,42);

10.2.3 排序算法

排序算法:对容器内部的元素进行排序的算法

介绍两个算法,一个为sort,一个为unique,sort将使用<运算符,排序容器中的元素。

unique算法,将不相同的元素排列在前面,然后返回不重复元素的尾后迭代器。

10.3 定制操作

10.3.1

很多算法都使用了元素的小于<或者等于==运算符。但是有些元素并没有定义这两个运算符。此时,如果需要使用泛型算法,就需要向泛型算法,传递一个操作,来代替默认的小于或者等于运算符,这个操作,称为谓词。

为了达到和小于或者等于相同的效果,这个操作需要满足下面几个条件:

  1. 可调用
  2. 返回类型可用作条件表达式。

满足上面条件的,可以是一个自定义的函数,也可以是一个可调用的对象,还可以是一个lambada表达式。

下面先记下函数这种类型,后面会有lambada和可调用对象的笔记

如果我们想要sort按照字符串的长度进行排序。那么就不能使用默认的小于运算符,此时我们需要传递给sort一个操作。这个操作需要满足上面的两个条件,这里以函数为例:

bool isShorter(const string &s1,const string &s2){
    return s1.size()<s2.siz();
}

//按照长度排序
sort(words.begin(),words,end(),isShorter);

在上面的例子中,可能疑惑的有两点,我如何知道自定义函数的形参的个数和类型???

个数:每一个算法(此例中是sort)都规定了,他能接受的操作(此例中为isShorter)的参数个数,sort规定为两个

类型:传递过来的操作(此例中为isShorter),接受的参数类型为容器的元素类型

10.3.2 lambda 表达式

一个算法规定了,自定义操作,所能接受的参数个数。如果自定操作,需要更多额外的信息,需要外部传递进来,此时使用lambda更加合适

lambda

可以这样理解lambda,他生成一个匿名的可调用对象。语法格式如下:

[捕获列表](参数列表)->返回类型{执行代码}

捕获列表表示:生成的匿名对象需要访问的局部变量,因为一个对象无法访问局部变量。所以需要将局部变量放在捕获列表中。

参数列表:生成的匿名对象,被调用时,需要传递的实参参数类型列表

返回类型:即可调用对象,调用完成之后返回的类型

例子:

auto f = [] {return 42;};

cout << f() << endl;//输出42

当参数列表为空的时候,可以忽略
如果忽略掉返回类型,则编译器从执行代码中推断,这要求执行代码,只有一条return语句。否则,编译器推断其返回类型为void

注意:如果lambda 的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void

假如,sz是一个局部变量,出现在一个函数内部,例如下面的代码:

[](const string & a){
    return a.size() >= sz;
}

因为,上面的lambda是一个匿名对象,这个对象对局部对象无法进行直接访问,所以,在执行代码内部,直接使用sz是错误的。此时应该使用捕获列表。如下:

[sz] (const string &a){
    return a.size() >= sz;
}

10.3.3 lambda捕获和返回

现在来思考一下,lambda的实现。在后面的笔记中将会详细介绍lamda的实现。这里先给出一个感性的概念。

编译器对lambda会生成一个未命名的类类型。当我们使用这个lambda表达式时,使用的是这个未命名类类型的一个对象。

那么这个类类型,为了能够访问局部变量,需要把局部变量放入捕获列表中。

那么这个类类型是如何达到这种效果的呢?

实际上是,类类型中定义相应的数据成员。这些数据成员的值就是捕获列表中的局部变量的值。所以这个未命名类型的对象访问自己的成员,就相当于访问了捕获列表中的变量。

那么变量有这么几种类型存在:引用,指针,拷贝

而在lambda中生成的类类型,就可以让捕获列表,使用引用和,拷贝这两种。他们分别称为引用捕获和值捕获,引用捕获只需要在需捕获的变量前面加上取地址符即可。见下面例子

值捕获的例子:

void fcn1(){
    size_t v1 =42;
    auto f = [v1] {return v1;};
    v1 = 0;
    auto j= f();//j为42
}

引用捕获的例子

void fcn2(){
    size_t v1 = 42;
    auto f2 = [&v1]{return v1;}
    v1 = 0;
    auto j = f2();//j为0
}

注意:尽量保持lamda的变量捕获简单明了

隐式捕获

除了自己能够显示在捕获列表中写下需要捕获的变量以外,还可以让编译器智能决定哪些变量需要捕获。只需要在捕获列表中,写上&或者=,就表示让编译器智能决定需要捕获的变量

当然,也可以即使用显示捕获,也可以使用隐式捕获。但是有一个前提:隐式捕获和显示捕获的方式必须是不同。也就是说,当隐式捕获是引用捕获时,显示捕获必须是值捕获;当隐式捕获是值捕获的时候,显示捕获必须是引用捕获。

可变lambda

默认情况下,lambda不准改变其捕获的值。如果想要改变,需要使用mutable关键字。如下:

void fcn3(){
    size_t v1 = 42;
    auto f = [v1] () mutable {return ++v1;};
    v1 = 0;
    auto j = f();//j为43
}

lambda返回类型
考虑下面的写法:

transform(vi.begin(),vi.end(),vi.begin(),
[](int i){return i < 0? -i:i;});

再将上面的用法写成如下的形式:

transform(vi.begin(),vi.end(),vi.begin(),
[](int i){if(i<0) return -i;else return i;});

前面提到:如果lambda执行体的语句含有多个return语句,且没有指明返回类型,那么编译器将认为返回类型为void

上面第二个例子,返回类型为void,他是错误的。正确的写法应该是第一种写法。

bind参数绑定

auto newCallable = bind(callable,arg_list);

bind输入一个可调用对象,输出也是一个可调用对象,但是,可以根据arg_list参数,进行灵活处理

更一般的例子如下:

using std::placeholder::_1;
using std::placeholder::_2;
auto g = bind(f,a,b,_2,c,_1);

//等价于调用f(a,b,3,c,1)
g(1,3);

注意:上面使用的_1,定义在std::placeholder命名空间中,如果想方便使用,需要加上:

using namespace std::placeholder;

绑定引用参数

思考下面的例子:

//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来调用print如下

for_each(words.begin(),words.end(),bind(print,os,_1,' ');

上面这个例子,出现一个问题,因为print第一个参数是一个引用,而bind传递过来的参数,确是值传递。下面才是正确的写法

for_each(words.begin(),words.end(),bind(),bind(print,ref(os),_1,' '));

上面例子中使用了ref函数,这个函数返回一个对象,这个对象包含给定的引用,并且这个对象是可以拷贝的。同样的会有一个cref函数,生成一个保存const引用的对象。

10.4 再探迭代器

除了前面记下的迭代器以外,标准库文件iterator中还定义了额外的几种迭代器。如下:

  1. 插入迭代器
  2. 流迭代器
  3. 反向迭代器
  4. 移动迭代器

10.4.1 插入迭代器

为插入迭代器赋值,就相当于给这个迭代器所指的容器,插入一个新值。

插入迭代器有下面三种,他们的不同,主要在于插入的位置。

  1. back_inserter : 创建一个使用push_back的迭代器,只有容器支持push_back的时候,才可以使用
  2. front_inserter : 创建一个使用push_front的迭代器, 只有容器支持front_back的时候,才可以使用
  3. inserter :创建一个插入迭代器,此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

下面给出插入迭代器的赋值,自增,等运算符的操作。

c++ primer 第五版 笔记 第十章_第1张图片

inserter说明如下:

*it = val;
//等价于
it = c.insert(it,val);//it指向新加入的元素
++it;//递增it使他指向原来的元素

front_inserter说明如下:

list<int> lst = {1,2,3,4};
list<int> lst2,lst3;
//lst2 包含 4 3 2 1
copy(lst.cbegin(),lst.end(),front_inserter(lst2));
//lst 3 包含 1 2 3 4
copy(lst.cbegin(),lst.end(),inserter(lst3,lst3.begin()));

10.4.2 流迭代器

两种类型的流迭代器:

  1. istream_iterator 读取输入流
  2. ostream_iterator 向输出流写入数据

istream_iterator 操作

创建流迭代器的时候,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。因此,istream_iterator要读取的类型必须定义了输入运算符。

当创建一个istream_iterator时,可以将它绑定到一个流。如果没有,则代表创建了一个可以当做尾后使用的迭代器

例如:

istream_iterator<int> int_it(cin); //从cin 读取int
istream_iterator<int> int_eof;//尾后迭代器

ifstream in("afile");
istream_iterator<string> str_it(in);//从afile中读取字符串

下面是一个用istream_iterator从标准输入中读取的例子。

istream_iterator<int> in_iter(cin);
istream_iterator<int> eof;

while(in_iter != eof)
    vec.push_back(*in_iter++);

我们还可以使用如下的代码,达到同样的目的

istream_iterator<int> in_iter(cin),eof;
vector<int> vec(in_iter,eof);

上例中,用一个迭代器来构造一个vector,而这对迭代器指向了输入流,表示从输入流中,读取并构造vector

下面给出输入流迭代器常见的操作

c++ primer 第五版 笔记 第十章_第2张图片

istream_iterator允许使用懒惰求值

当将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。标准库中的实现保证:在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成了。

对于大多数程序来说,立即读取还是延迟读取没什么差别。但是,如果我们创建了一个istream_iterator,没有使用就销毁了,或者我们正在从两个不同的对象同步读取同一个流,那么何时读取可能就很重要了。

ostream_iterator操作

所有具有输出运算符的类型,都可以定义相应的ostream_iterator.当创建一个ostream_iterator时,我么可以提供第二个可选的参数,这个参数是一个字符串。在输出每个元素之后,都会打印这个字符串。

此字符串必须是一个c风格的字符串。必须将ostream_iterator绑定到一个指定的流,不允许空的或者表示尾后位置的ostream_iterator

下面给出ostream_iterator的操作

c++ primer 第五版 笔记 第十章_第3张图片

例子如下:

ostream_iterator<int> out_iter(cout," ");
for(auto e:vec)
    *out_iter++ = e;
cout << endl;

此程序中的每个元素都写到了cout中,每个元素后面加一个空格。

由上面的表格可以知道,还可以写成下面这种形式

for(auto e:vec)
    out_iter = e;
cout << endl;

但是推荐第一种写法,迭代器的使用与其他的保持一致。如果想改成其他迭代器的操作,修改起来较容易。

下面这种写法更简单

copy(vec.begin(),vec.end(),out_iter);
cout << endl;

反向迭代器

反向迭代器:在容器中从尾元素向首元素移动的迭代器。

这样递增一个反向迭代器,会移动到前一个,递减一个反向迭代器会移动到下一个。

除了forward_list以外,其他容器都支持反向迭代器,可以使用rbegin,rend,crbegin,crend来获取。

例子如下:

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 6 5 4 3 2 1 0

注意:反向迭代器需要递减运算符

因此,对于forward_list和流迭代器来说,不能创建对应的反向迭代器

注意:获取反向迭代器对应的普通迭代器,使用其base成员。

10.5 泛型算法结构

因为实在不知怎么做笔记好,直接复制吧,又太多,还是直接参考原书即可

《primer c++ 第五版,中文版》10.5小节

10.6 特定容器的算法

标准库提供的算法,可以用于list和forwrd_list,但是消耗太大,因为这些算法通常需要移动元素。但是对于链表来说,不需要移动元素,只需要移动链接就可以,因此链表提供了自己的一套的算法。

下面给出他们的操作
c++ primer 第五版 笔记 第十章_第4张图片
c++ primer 第五版 笔记 第十章_第5张图片

你可能感兴趣的:(英文翻译)