第十章-泛型算法

泛型算法


1 概述

  • 迭代器令算法不依赖于容器,但是算法依赖于元素类型的操作。
  • 泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个编程假设:算法永远不会改变底层容器的大小。


2 初始泛型算法

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

2-1 只读算法

  • 那些只接受一个单一迭代器来表示第二序列的算法,都假定第二序列至少与第一序列一样长。

2-2 写容器元素的算法

  • 一种保证算法有足够元素空间来容纳输出元素的方法是使用插入迭代器。
  • 作为插入迭代器的一种,back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。
  • copy返回的是其目的位置迭代器(递增后)的值。

2-3 重排容器元素的算法

  • unique算法重排输入序列,将相邻的重复项“消除”(只是将不重复部分放在序列开始,重复部分放最后),并返回一个指向不重复范围末尾的迭代器。


3 定制操作

3-1 向算法传递函数

  • stable_sort是一种稳定排序算法,保持了相等元素的原有相对顺序。

3-2 lambda表达式

  • 我们可以向一个算法传递任何类别的可调用对象。对于一个对象或一个表达式,如果可以对其使用可调用运算符,则称它为可调用的。
  • 一个lambda表达式表示一个可调用代码,可以理解为一个未命名的内联函数。
  • 我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。
  • 如果忽略了返回类型,lambda根据函数体中的代码判断出返回类型。如果函数体只是一个return语句,则返回类型从返回表达式的类型判断而来;否则,返回类型为void。
  • 一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
  • 一个lambda可以直接使用定义在当前函数之外的名字。

3-3 lambda捕获和返回

  • 当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名)类类型。
  • 与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。
  • 当我们混合使用隐式捕获(只有=或&)和显式捕获(指定要捕获变量名称,或变量前加&)时,捕获列表中的第一个元素必须是一个&或=,此符号指定了默认捕获方式为引用或值。而且,显式捕获的变量必须使用与隐式捕获不同的方式。
  • 如果我们希望改变一个捕获的变量值,就必须在参数列表首加上关键字mutable。

3-4 参数绑定

  • C++11的bind函数,可以看作为一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
  • auto newCallable = bind(callable,arg_list);newCallable本身是一个可调用对象,arg_list是参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传递它arg_list中的参数。
  • arg_list中参数可以包含形如_n的名字,其中n是个整数。这些参数叫“占位符”(placeholders),表示newCallable的参数(如:_1为newCallable第一个参数,_2为newCallable第二个参数)。
  • 默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,与lambda类似,有时对有些绑定参数我们希望以引用的方式传递,或是要绑定的参数的类型无法拷贝。这时候就必须使用标准库ref函数,它返回一个对象,包含给定的引用,此对象时可拷贝的。还有一个cref函数,生成相应的const引用的对象。


4 再探迭代器

特殊迭代器
插入迭代器 被绑定到一个容器上,可用来向容器插入元素
流迭代器 被绑定到输入/输出流上,可用来遍历所关联IO流
反向迭代器 向后而不是向前移动,除了forward_list,容器都拥有它
移动迭代器 不是拷贝其中的元素,而是移动它们

4-1 插入迭代器

插入迭代器操作
it = t 在it指定的当前位置插入t,根据it绑定容器的不同分别会调用push_back,push_front或insert
*it,++it,it++ 这些操作虽然存在,但不会对it做任何事情。每个操作都返回it

4-2 iostream迭代器

  • 当我们将一个iostream_iterator绑定到一个流上时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到使用迭代器才真正读取。标准库中的实现所保证的是,在第一次解引用迭代器之前,从流中读取数据的操作已经完成。这叫懒惰求值。
  • 对于ostream_iterator来说,*out,++out和out++这些操作都是存在的,但跟插入迭代器一样,不对out做任何事情。每个操作都返回out。
  • 只有类型定义了>>或<<运算符时,我们才可以为其创建iostream对应的迭代器。

4-3 反向迭代器

  • 我们只能从既支持++也支持--的迭代器来定义反向迭代器。流迭代器不支持递减运算,因此不可能从一个forward_list或一个流迭代器创建反向迭代器。
  • 如果想从反向迭代器获取其对应的普通迭代器,可以调用reverse_iterator的base成员函数。
  • 反向迭代器的目的是表示元素范围,而这些范围是不对称的(因为都要符合左闭合区间),这导致:当从一个普通迭代器初始化一个反向迭代器,或者给一个反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同的元素。


5 泛型算法结构

算法要求迭代器类别
输入迭代器 只读,不写;单遍扫描,只能递增
输出迭代器 只写,不读;单遍扫描,只能递增
前向迭代器 可读写;多遍扫描,只能递增
双向迭代器 可读写;多遍扫描,可递增递减
随机访问迭代器 可读写,多遍扫描,支持全部迭代器运算

5-1 5类迭代器

  • 输入迭代器:可以读取序列中的元素,必须支持:
  • 用于比较两个迭代器的相等和不相等运算符(==、!=);
  • 用于推进迭代器的前置和后置递增运算(++);
  • 用于读取元素的解引用运算符(*);解引用只会出现在赋值运算符的右侧
  • 箭头运算符(->),等价于(*it).member。
  • 输出迭代器:可以看做输入迭代器功能上的补集——只写而不读元素,必须支持:
  • 用于推进迭代器的前置和后置递增原酸(++);
  • 解引用运算符(*),只出现在赋值运算的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)。
  • 前向迭代器:支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。
  • 双向迭代器:支持前向迭代器所有操作,还支持前置和后置的递减运算符(--)。
  • 随机访问迭代器:提供在常量时间内访问序列中任意元素的能力。支持双向迭代器的所有功能,此外还支持:
  • 用于比较两个迭代器相对位置的关系运算符(<、<=、>和>=);
  • 迭代器和一个整数值的加减运算(+、+=、-和-=);
  • 用于两个迭代器的减法运算符(-);
  • 下标运算符(iter[n])。

5-2 算法形参模式

5-3 算法命名规范

  • 一些算法使用重载形式传递一个谓词
  • _if版本的算法
  • 区分拷贝元素的版本和不拷贝的版本


6 特定容器算法

  • 对于list和forward_list,应该优先使用成员函数版本而不是通用算法。
  • 链表特有版本与通用版本算法之间的一个至关重要的区别是前者会改变底层的容器。

你可能感兴趣的:(C++Primer无聊笔记)