C++ prime(第五版) 第十章 小结

1.1 概述

泛型算法:实现了一些经典算法的公共接口,如排序和搜索;之所以称为“泛型的”,是因为它们可以用于不同类型的元素和多种容器类型(不仅包括标准库类型,如vector或list,还包括内置的数组类型),还能用于其他类型的序列。大多数算法定义在头文件algorithm中,标准库还在头文件numeric中定义了一组数值泛型算法。

关键一点在于,泛型算法不会直接调用容器的操作,而是通过迭代器来访问,修改,移动元素。

虽然迭代器令算法不依赖于容器,但是算法依赖于元素类型的操作。如find用元素类型的==运算符完成每个元素与给定值的比较。但如Sales类就未定义“==‘’。

泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。算法永远不会改变底层容器的大小。,即永远不会直接添加或删除元素(需要借助插入器实现)。

1.2 初识泛型算法(标准库提供100+个算法)

     除少数例外,标准库算法都对一个范围内的元素进行操作。我们将此元素范围称为“输入范围”。接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。

     理解算法最基本的方法就是了解他们是否读取元素、改变元素或是重排元素顺序。

1、只读算法

   只读算法只会读取输入范围内的元素,而从不改变元素。find就是如此。

   只读算法accumulate(定义在头文件numeric中)接受三个参数,前两个指出需要求和的元素范围,第三个参数是和的初值(如 int sum = accumulate(vec.cbegin(), vec.cend(), 0))。accumulate的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。其中也蕴含了一个编程假设:将元素类型加到和的类型上的操作必须是可行的。。即,序列中的元素的类型必须与第三个参数匹配,或是能转换。对于只读取而不改变元素的算法,通常最好使用cbegin()和cend()。

    只读算法equal,用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素与第二个序列中对应的元素进行比较。如果对应的元素都相等返回true,若不等则返回false。此算法接受三个迭代器:前两个表示第一个序列的范围,第三个表示第二个序列的首元素。equal基于一个非常重要的假设:它假定第二个序列至少与第一个序列一样长。

2、写容器元素的算法

    当我们使用写容器元素的算法时,必须注意确保序列原大小至少不小于我们要求算法写入的元素数目。

    算法fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill将给定的这个值赋予输入序列中的每个元素(fill(vec.begin(), vec.end(), 0) //将每个元素重置为0)。fill_n接受一个单迭代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素(fill_n(dest, n, val)  //fill_n假定dest指向一个元素,而从dest开始的序列至少包含n个元素)。一个非常容易犯的错就是在一个空容器上调用fill_n。

    一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器。是一种向容器中添加元素的迭代器。back_inserter(定义在头文件iterator中)接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。     

vector<int> vec;

auto it = back_inserter(vec);

*it = 42;//vec中现在有一个元素,值为42

常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。

vector<int> vec;

fill_n(back_inserter(vec), 10, 0);//添加十个元素到vec,每个值均为0;

      拷贝(copy)算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含与输入序列一样多的元素。

      replace算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。此算法接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。将所有等于第一个值的元素替换为第二个值。如果希望保留原序列不变,可以调用replace_copy。replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);//此调用后,ilst并未改变,ivec包含ilst的一份拷贝,不过原来在ilst中值为0的元素在ivec中都变为42。

      泛型算法对于容器的要求并不是有足够的空间,而是足够的元素。

      严格来说,标准库算法根本不知道有“容器”这个东西。它们只接受迭代器参数运行于这些迭代器之上,通过这些迭代器来访问元素。因此,当你传递给算法普通迭代器时,这些迭代器只能顺序或随机访问容器中的元素,造成的效果就是算法只能读元素、改变元素值、移动元素,但无法添加或删除元素。但当我们传递给算法插入器时,例如back_inserter,由于这类迭代器能调用下层容器的操作来向容器插入元素,造成的算法执行的效果,就是向容器中添加元素。因此,关键要理解,标准库算法从来不直接操作容器,它们只操作迭代器。从而间接访问容器。能不能插入和删除元素,不在于算法,而在于传递给它们的迭代器是否具有这样的能力。


3、重排容器元素的算法

     调用sort会重排输入序列中的元素,使之有序,它是利用元素类型的<运算符来实现排序。

     可以使用一个称为unique的标准库算法来重排vector,使得不重复的元素出现在vector的开始部分。unique算法重排输入序列,将相邻的重复项“消除”,并返回一个指向不重复值范围末尾的迭代器。其实并未消除任何元素,只是覆盖相邻的重复元素。要实现删除操作还得利用vector的erase成员来完成。

1.3 定制操作

     很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的<或==运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。


1、向算法传递函数

    谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。分为一元谓词(只接受单一参数),和二元谓词(有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。

    排序算法中的stable_sort(stable_sort(words.begin(), words.end(), isShorter)//按长度排序,长度相同的单词维持字典序)。

2、lambda表达式

    有时我们希望进行的操作需要更多的参数,而不仅仅只是谓词的一个到两个参数。

    find_if算法接受一对迭代器,表示一个范围。但与find不同的是,find_if的第三个参数是一个谓词。find_if算法对输入序列的每个元素调用给定的这个谓词。它返回第一个使谓词返回非0值的元素,如果不存在这样的元素,则返回尾迭代器。

    一个lambda表达式表示一个可调用的代码单元。我们可以理解为一个未命名的内联函数。具有如下形式:

      [捕获列表](参数列表)->返回类型  {函数体}

      捕获列表是一个lambda所在函数中定义的局部变量的列表(通常为空),空捕获列表表明lambda不使用它所在函数中的任何局部变量。与普通函数不同的是lambda必须使用尾置返回来指定返回类型。我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符(auto f = [ ]{ return 42 ;} ; cout << f( ) << endl; //打印42   )。

如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。如果函数体只有一个return 语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为void。

与普通函数不同的是,lambda不能有默认参数。因此,一个lambda调用的实参数目永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。

虽然一个lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。一个lambda通过将局部变量包含在其捕获列表中指出将会使用这些变量。

for_each算法。接受一个可调用对象,并对输入序列中的每个元素调用此对象(for_each(wc, words.end(), [ ](const string&s) {cout << s << " ";});//打印单词,每个单词后面接一个空格)。捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字。

3、lambda捕获与返回

(1)捕获

 类似参数传递,变量的捕捉方式也可以是值或引用。

与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。

引用捕获与返回引用有着相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。

如果我们捕获一个指针或者迭代器、或采用引用捕获方式,就必须确保在lambda执行时,绑定到迭代器、指针或引用的对象仍然存在。一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用。

除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。当我们混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。

默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们洗完能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。如:

void fcn3()

{

     size_t v1 = 42;

auto f = [v1] ( ) mutable { return ++v1;};

v1 = 0;

auto j = f( ); // j为43 

}

而一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个const类型还是一个非const类型。

(2)指定lambda返回类型

如果一个lambda体包含return 之外的任何语句,则编译器假定此lambda返回void。

transform(vi.begin( ), vi.end( ), vi.begin( ), [ ](int i) { if(i < 0) return -i; else return i;} );//错误:不能推断lambda的返回类型。

当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型。上述可改为:

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

4 、参数绑定

对于那种只有一两个地方使用的简单操作,lambda表达式是最有用的。但是,如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。对于捕获列表为空的lambda,用函数来代替是比较容易的。但是对于捕获局部变量的lambda,用函数来替换它就不是那么容易了。


bind为标准库函数,定义在头文件functional中。可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

bind一般形式如下:auto newcallable(可调用对象) = bind(callable, arg_list);arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newcallable时,newcallable会调用callable,并传递给它arg_list中的参数。arg_list中的参数可能包含形如_n的名字,其中n是一个整数。这些参数是“占位符”,表示nawcallable的参数。_1表示newcallable的第一个参数,_2表示newcallable的第二个参数。名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间中。_1的声明可如下:using  std::placeholders::_1; 都可以是:using namespace  std::placeholders;

auto check6 = bind(check_size, _1, 6);//check6是一个可调用对象,接受一个string类型的参数,并用此string和值6来调用check_size.此bind调用只有一个占位符,表示check6只接受单一参数。占位符出现在arg_list的第一个位置,表示check6的此参数对应check_size的第一个参数。此参数是一个const string&。因此,调用check6必须传递给它一个string类型的参数,chek6会将此参数传递给check_size.

标准库中的bind函数在实际工作函数外做一层“包装”。它接受一个可调用对象A,即实际的工作函数,返回一个新的可调用对象B,供算法使用。A后面是一个参数列表,即传递给它的参数列表。其中有一些名字形如_n的参数,表示程序调用B时,传递给它的第几个参数。也就是说,算法调用B时传递较少的参数,B再补充其他一些值,形成更长的参数列表,从而解决算法要求的参数个数比实际工作函数所需参数个数少的问题。例如:auto g = bind(f, a, b, _2,  c, _1 );   等价于 g(_1, _2) 映射为 f(a, b, _2, c, _1) 对g的调用会调用f,用g的参数代替占位符,再加上绑定的参数a, b和c。

如果我们希望传递给bind一个对象而又不拷贝它,就必须使用标准库ref函数。

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

ostrem &print(ostream &os, const string &s, char c)

{

return os << s << c;

}

函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个cref函数,生成一个保存const引用的类。ref和cref均定义在头文件functional中。

1.4 再探迭代器

标准库再头文件iterator中还定义了额外几种迭代器。插入迭代器,流迭代器,反向迭代器,移动迭代器。

1、插入迭代器

back_inserter创建一个使用push_back的迭代器。总插入到容器尾元素之后。

front_inserter创建一个使用push_front的迭代器。总插入到容器首元素之前。

inserter创建一个使用inserter的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

例如:it是由inserter生成的迭代器,则赋值语句 *it = val;等价于   it = c.insert(it, val);//it指向新加入的元素 ++it;//递增it使它指向原来的元素

使用front——inserter向容器插入一些元素,元素最终在容器中的顺序与插入顺序相反,但back_inserter和inserter则不会。


2、iostream迭代器

istream_iterator迭代器读取输入流,outstream_iterator迭代器向流写入数据。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。

istream_iterator

istream_iterator要读取的类型必须定义了输入运算符。当创建一个istream_iterator时,我们可以将它绑定到一个流。我们也可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器。如:

(1)

istream_iterator<int> in_iter(cin); //从cin读取int

istream_iterator<int> int_eof; // 尾后迭代器

(2)

istream_iterator<int> in_iter(cin), eof; //从cin读取int

vector<int> vec(in_iter, eof);  // 从迭代器范围构造vec

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

ostream_iterator

当创建一个ostream_iterator时, 我们可以提供(可选的)第二个参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个c风格字符串(即,一个字符串字面常量或者一个指向以空字符结尾的字符数组的指针)。必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostrem_iterator。

表10.4注意一点:ostream_iterator<T> out(os);  out = val;//用<<运算符将val写入到out所绑定的ostream中。val的类型必须与out可写的类型兼容。

如:

ostream_iterator<int> out-iter(cout, " ");

for(auto e : vec)

     *out_iter ++ = e;//赋值语句实际上将元素写到cout。赋值时,实际可以忽略解引用和递增运算。但还是保持这种写法,以 cout << endl;                             便与其他迭代器的使用保持一致。

(2)

调用copy打印vec中的元素:

copy(vec.begin(), vec.end(), out_iter);

cout<<endl;

3、反向迭代器

反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。。递增一个反向迭代器(++it)会移动到前一个元素;递减一个迭代器(--it)会移动到下一个元素。除forward_list之外,其他容器都支持反向迭代器。我们可以通过调用rbegin、rend、crbegin和crend成员函数来获得反向迭代器。图10.1很重要,记住.rbegin( )、.rend( )、.crbegin( )和.crend( )迭代器各自所指的位置。

我们只能从既支持++也支持--的迭代器来定义反向迭代器。因此,不可能从一个forward_list或一个流迭代器创建反向迭代器。

我们通过调用reverse_iterator的base成员函数来完成一个反向迭代器转换回一个普通的迭代器。但是要注意的是:反向迭代器与普通迭代器的转换是左闭合区间的转换,而非精确位置的装换。反向迭代器转换为普通迭代器时,会指向靠近容器尾反向的邻居。反之,由普通迭代器转换为反向迭代器,则反向迭代器会指向靠近容器首方向的邻居。

例如练习10.37 表明正序迭代器转变为逆序迭代器的一种方式 

vector<int>::reverse_iterator re(vi.begin( ) + 2);//将vi[2],也就是第3个元素的位置转换为反向迭代器。re是反向结束的意思。

vector<int>::reverse_iterator rb(vi.begin( ) + 7);//将vi[7],也就是第8个元素的位置转换为反向迭代器。rb是反向开始的意思。

1.5泛型算法的结构

  1、5类迭代器

 迭代器类别分为:输入迭代器(只用于顺序访问),输出迭代器、前向迭代器、双向迭代器、随机访问迭代器。是按照是否读、写或是重排序列中的元素来分类。各个迭代器具体支持的功能详见书本P366.

2、算法形参模式


四种形式:

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被绑定到一个插入迭代器或是一个ostream_iterator。插入迭代器会将新元素添加到容器中,因而保证空间是足够的。ostream_iterator会将数据写入到一个输出流,同样不管要写入多少个元素都没有问题。

接受单独beg2的算法假定从beg2开始的序列与beg和end所表示的范围至少一样大。

3、算法命名规范

接受为此参数来代替<或==运算符的算法,以及那些不接受额外参数的算法,通常都是重载的函数。函数的一个版本用元素类型的运算符来比较元素;而另一个版本接受一个额外的谓词参数,来代替<或==:

unique(beg, end);//使用==运算符比较元素

unique(beg, end, comp);//使用comp比较元素

接受一个元素值的算法通常有另一个不同名的(不是重载的)版本,该版本接受一个谓词代替元素值:

find(beg, end, val);//查找输入范围中的val第一次出现的位置

find_if(beg, end, pred);//查找第一个令pred为真的元素


reverse(beg, end);//反转输入范围中元素的顺序

reverse_copy(beg, end, dest);//将元素按逆序拷贝到dest

1.6特定容器算法

链表类型list和forward_list定义了几个成员函数形式的算法,它们定义了独有的sort,merge,remove,reverse和unique,详见表10.6表10.7。链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器。






    

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值