菜狗的C++ primer读书笔记:第十章 泛型算法

来源:互联网 发布:android 监听数据变化 编辑:程序博客网 时间:2024/06/05 18:24

1.     标准库并未给每个容器添加大量的功能,而是提供了一组算法,这些算法中的大多数都独立于任何特定的容器,这些算法是通用的,它们可用于不同类型的容器和不同类型的元素。

2.     大多数算法都定义在头文件algorithm中,标准库还在头文件numeric中定义了一组数值泛型算法。

3.     一般情况下算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。

*常用算法find:

传递给find的前两个元素是表示元素范围的迭代器,第三个参数是一个值。find将范围中的每个元素与给定值进行比较。它返回指向第一个等于给定值的元素的迭代器,如果范围中无匹配元素则返回第二个参数表示搜索失败。

4.     迭代器令算法不依赖于容器,使得算法永远不会执行容器的操作。它们只会运行于迭代器之上,执行迭代器的操作。这个特性带来一个编程假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素。   

5.     虽然迭代器的使用令算法不依赖于容器的类型,但大多数算法都使用了一个或多个元素类型上的操作。且大多数算法提供一种方法允许我们使用自定义的操作来代替默认运算符。

6.     虽然大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同。理解算法最基本的方法就是了解它们是否读取元素、改变元素或是重排元素顺序。按照使用元素方式的不同可分为一下三种算法。

7.     只读算法:只会读取其输入范围内的元素,从不改变元素。

*常用算法accumulate:

accumulate函数接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。

*常用算法equal:

它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果所有对应元素都相等,则返回true,否则返回false。此算法接受三个迭代器:前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素。

8.     写入算法:将新值赋予序列中的元素的算法。当使用这类算法时,必须确保序列原大小至少不小于我们要求算法写入的元素数目。

*常用算法fill:

接受一对迭代器表示一个范围,还接受一个值作为第三个参数。Fill将给定的这个值赋予输入序列中的每个元素。由于其向给定输入序列写入数据,因此只要我们传递的是有效输入序列,写入操作就是安全的。

9.     一些算法接受一个迭代器来指出一个单独的目的位置,这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。

*常用算法copy:

拷贝算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。要保证传递给copy的目的序列至少要包含与输入序列一样多的元素,这一点很重要。此算法返回的是其目的位置迭代器递增后的值。

10.  多个算法都提供所谓的“拷贝”版本,这些算法计算新元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果。

11.  重排算法:某些算法会重排容器中元素的顺序。

*常用算法unique:

此算法重排输入序列,将相邻的重复项“消除”,并返回一个指向不重复值范围末尾的迭代器。unique并不真的删除元素,因为算法不能执行容器的操作,我们可以使用容器操作借助算法返回的迭代器来删除重复元素。

12.  很多算法都会比较输入序列中的元素,除默认情况外标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。

13.  谓词是一个可调用的表达式,返回结果是一个能用作条件的值。标准库算法使用两类谓词:一元谓词和二元谓词。接受谓词参数的算法对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型。

*常用算法sort:

有两个版本,一个版本接受一个迭代器对,默认使用元素类型的<运算符排序。当我们希望的排序顺序与<定义的不同时,或者我们的序列可能保存的是未定义<运算符的元素类型,在这两种情况下,我们需要重排sort的默认行为,接受一个二元谓词作为第三参数的sort版本用这个谓词来代替<比较元素。

另外还有一个stable_sort算法,这种稳定排序算法维持了相等元素的原有顺序。具体用法同sort相同。

*常用算法find_if:

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

*常用算法for_each:

此算法接受一个可调用对象,并对输入序列中每个元素调用此对象

14.  我们可以向一个算法传递任何类别的可调用对象。对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。可调用对象有四种:函数、函数指针、重载了函数调用符的类、lambda表达式。

一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与函数类似,它具有一个返回类型、一个参数列表、一个函数体;与函数不同的是lambda可能定义在函数内部,存在捕获列表是一个lambda所在函数中定义的局部变量的列表(通常为空),且当需要定义返回类型时必须使用尾置返回类型

形式如下:

[capture list](parameter list) -> return type { function body}

 

我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。

在lambda表达式中忽略括号和参数列表等价于指定一个空参数列表。

在lambda表达式中忽略返回类型,当函数体中只是一个return语句则返回类型从返回的表达式类型推断而来,否则返回类型为void。

15.  当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)新类型。当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的还有有使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类的对象。默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员,类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。

16.  值捕获:与传值参数类似,lambda采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。由于被捕获的变量值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值。

引用捕获:一个以引用方式捕获的变量与其他任何类型的引用行为类似。引用捕获与返回引用有着相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须保证被引用的对象在lambda执行的时候是存在的。

17.  不同的构造捕获列表的方式


18.  如果希望改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。因此可变lambda表达式可以省略参数列表。

19.  对于只在一两个地方使用的简单操作,lambda表达式时最有用的。如果我们需要在很多地方使用相同的操作或者一个操作需要很多语句才能完成,通常使用函数更好。

20.  参数绑定:

可以将标准库bind函数看做一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

调用的一般形式为:

auto newCallable = bind(callable ,arg_list);

 

       newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。即当我们调用newCallable时,newCallable会调用callable。并传递给它arg_list中的参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个整数。这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表示生成的可调用对象中参数的位置。

除此之外还可以用bind绑定给定可调用对象中的参数或重新安排其顺序。

21.  默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。如果我们希望传递bind一个对象而又不拷贝它,就必须使用标准库ref函数。函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个cref函数,生成一个保存const引用的类。

22.  除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外集中迭代器,包括插入迭代器、流迭代器、反向迭代器、移动迭代器。

23.  插入迭代器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现想给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代期调用容器操作来向给定容器的指定位置插入一个元素。

插入迭代器的类型:


插入迭代器的操作:


需要注意的是,只有在容器支持push_front或者支持push_back的情况下,我们才能使用front_inserter或back_inserter。

front_inserter生成的迭代器会将插入的元素序列的顺序颠倒过来,而inserter和back_inserter则不会。

24.  虽然iostream类型不是容器,但标准库定义了可以用于这些IO类型对象的迭代器。istream_iterator读取输入流,ostream_iterator向一个输出流写数据。这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其中写入数据。

istream_iterator的操作:


对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。

由于算法使用迭代器操作来处理数据,而流迭代器又至少支持某些迭代器操作,因此我们至少可以用某些算法来操作流迭代器。

istream_iterator允许使用懒惰求值(Lazy Evaluation,具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。标准库中的实现所保证的是,在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成。

      

ostream_iterator的操作:


创建此迭代器时,我们可以提供(可选的)第二参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个C风格字符串。

必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator。

运算符*和++虽然对ostream_iterator对象不做任何事情,但推荐使用它们的写法,因为这会使得流迭代器的使用与其他迭代器的使用保持一致。

25.  反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增和递减操作的含义会颠倒过来。除了forward_list之外,其他容器都支持反向迭代器,流迭代器也不能创建反向迭代器。我们通过调用rbegin、rend、crbegin、crend成员函数来获得反向迭代器,这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。

我们通过调用reverse_iterator(反向迭代器)的成员函数base来完成反向迭代器到正向的转换,这两个迭代器指向的元素不同。

26.  任何算法的最基本的特性是它要求其迭代器提供哪些操作,每个算法都会对它的每个迭代器参数指明须提供哪类迭代器。算法所要求的迭代器操作可以分为5个迭代器类别:


迭代器是按它们所提供的操作来分类的,而这种分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持底层类别迭代器的所有操作。C++标准指明了泛型和数值算法的每个迭代器参数的最小类别,对每个迭代器参数来说,其能力必须与规定的最小类别至少相当,向算法传递一个能力更差的迭代器会产生错误。

27.  算法形参模式

大多数具有如下四种形式之一:

 

 

dest参数是一个表示算法可以写入的目的位置的迭代器。算法假定目标空间足够容纳写入的数据。常见情况是dest被绑定到插入迭代器或者ostream_iterator。

beg2或是beg2和end2参数表示第二个输入范围。算法假定从beg2开始的范围与beg和end所表示的范围至少一样大。

28.  算法命名规范

一些算法使用重载形式传递一个谓词。函数的一个版本用元素类型的运算符来比较元素;另一个版本接受一个额外谓词参数,来代替<或==。

接受一个元素值的算法通常有另一个不同名的(不是重载)的版本,该版本接受一个谓词来代替元素值。接受谓词参数的算法都有附加的_if前缀。

默认情况下重排元素的算法将重排后的元素写回给定的输入序列中。这些算法还提供另一个版本,将元素写到一个指定的输出目的位置。如我们所见,写到额外目的空间的算法都在名字后面附加一个_copy。

29.  与其他容器不同,链表类型list和forward_list定义了几个成员函数形式的算法,特别是定义了独有的sort、merge、remove、reverse和unique。通用版本sort要求随机访问迭代器,因为这两个类型分别提供双向迭代器和前向迭代器所以不能使用;而其他算法的通用版本虽然可以使用但代价很高,链表可以通过改变元素间的链接来快速“交换”元素,因此链表版本的这些算法性能比对应的通用版本好得多。

如下:


除此之外,链表类型还特有定义了splice算法:


*多数链表特有的算法都与其通用版本很相似,但不完全相同。链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器。

阅读全文
0 0
原创粉丝点击