第十章 泛型算法(重点)

来源:互联网 发布:西门子模拟量编程实例 编辑:程序博客网 时间:2024/05/20 21:43

10. 泛型算法(重点)

标准库容器定义的操作集合惊人得小。标准库并未给每个容器添加大量功能,而是提供了一组算法,这些算法中的大多数都独立于任何特定的容器。

10.1 概述

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

    泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个令人惊讶但非常必要的编程假定:算法永远不会改变底层容器的大小。

10.2 初识泛型算法

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

    只读算法 accumulate,它定义在头文件 numeric 中。accumulate 的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值类型。将空串当做一个字符串字面值传递给第三个参数是不可以的,会导致一个编译错误。由于 const char* 并没有 + 运算符,此调用也将产生编译错误。

    另一个人只读算法 equal 基于一个非常重要的假设:它假设第二个序列至少与第一个序列一样长。广义上,那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少于第一个序列一样长。用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。确保算法不会试图访问第二个序列中不存在的元素是程序员的责任。

    equal 使用 == 运算符比较两个序列中的元素。string 类重载了 ==,可比较两个字符串是否长度相等且其中元素对位相等。而 C 风格字符串本质是 char* 类型,用 == 比较两个 char* 对象,只是检查两个指针值是否相等,即地址是否相等,而不会比较其中字符是否相同。

  • 向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。

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

  • 调用 sort 会重排输入序列中的元素,使之有序,它是利用元素类型的 < 运算符来实现排序的。而 unique 的标准库算法使不重复的元素出现在容器的开始部分。标准库算法对迭代器而不是容器进行操作,使用容器操作来完成真正的删除操作。删除一个空范围没有什么不良后果,因此程序即使在输入中无重复元素的情况下也是正确的。

    泛型算法的一大优点是“泛型”,也就是一个算法可用于多种不同的数据类型,算法与所操作的数据结构分离。要做到算法于数据结构分离,重要的技术手段技术使用迭代器作为两者的桥梁。算法从不操作具体的容器,从而也就不存在与特定容器绑定,不适用于其他容器的问题。

    为了实现与数据结构的分离,为了实现通用性,算法根本就不该知道容器的存在。算法访问数据的唯一通道是迭代器。是否改变容器大小,完全是迭代器的选择和责任。

10.3 定制操作

  • 谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。通过调用

    stable_sort,可以保持等长元素间的字典序。标准库定义了名为 partition 的算法,它接受一个谓词,对容器内容进行划分,使得谓词为 true 的值会排在容器的前半部分,而使谓词为 false 的值会排在后半部分。

  • find_if 算法接受一对迭代器,表示一个范围。但与 find 不同的是,find_if 的第三个参数是一个谓词。我们可以向一个算法传递任何类别的可调用对象。对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。

    可调用的对象有函数、函数指针、重载了函数调用运算符的类和 lambda 表达式。一个 lambda 表达式表示一个可调用的代码单元。如何 lambda 调用的函数体包含任何单一 return 语句之外的内容,且未指定返回类型,则返回 void 。

    一个 lambda 调用的实参数目永远与形参数目相等。一个 lambda 通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引 lambda 在其内部包含访问局部变量所需的信息。一个 lambda 只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。

    一个 lambda 可以直接使用定义在当前函数之外的名字。捕获列表只用于局部非 static 变量,lambda 可以直接使用局部 static 变量和在它所在函数之外声明的名字。

  • 当定义一个 lambda 时,编译器生成一个与 lambda 对应的新的(未命名的)类类型。

    如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在 lambda 执行的时候是存在的。广义上,如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在 lambda 执行时,绑定到迭代器、指针或引用的对象仍然存在。

    为了指示编译器推断捕获列表,应在捕获列表中写一个 & 或 =。& 告诉编译器采用捕获引用方式,= 则表示采用值捕获方式。当我们混合使用隐式捕获和显示捕获时,捕获列表中的第一个元素必须是一个 & 或 =,此符号制定了默认捕获方式为引用或值。

    如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字 mutable。默认情况下,如果一个 lambda 体包含 return 之外的任何语句,则编译器假定此 lambda 返回 void。当我们需要为一个 lambda 定义返回类型时,必须使用尾置返回类型。

  • 对于那种只在一两个地方使用的简单操作,lambda 表达式是最有用的。

    using namespace namespace_name;//说明所有来来自 namespace_name 的名字都可以在我们的程序中直接使用。

    默认情况下,bind 的那些不是占位符的参数被拷贝到 bind 返回的可调用对象中。新的 C++ 程序应该使用 bind。

10.4 再探迭代器

  • 标准库头文件 iterator,额外定义了几种迭代器:插入迭代器、流迭代器、反向迭代器和移动迭代器。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。

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

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

    istream_iterator<int> in_iter(cin), eof//从 cin 读取 int
    vector<int> vec(in_iter, eof)//从迭代器范围构造 vec

    当我们将一个 istream_iterator 绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器才真正读取。

    istream_iterator<int> in(cin), eof;
    cout << accumulate(in, eof, 0) << endl;

    C 风格字符串(即,一个字符串字面常量或者一个指向以空字符结尾的字符数组的指针)。

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

    任何定义了输入运算符(<<)和输出运算符(>>)的类型都可创建 istream_iterator 或 ostream_iterator 对象。

  • 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。
    一个 forward_list 或一个流迭代器不可能创建反向迭代器。

    通过调用 reverse_iterator 的 base 成员函数完成其对应普通迭代器的转换。

    反向迭代器的目的是表示元素范围,而这些范围是不对称的,这导致一个重要的结果:当我们从一个普通迭代器初始化一个反向迭代器,或是给一个反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同的元素。

10.5 泛型算法结构

  • 任何算法的最基本的特性是它要求其迭代器提供哪些操作,算法还共享一组参数传递规范和一组命名规范。除了输出迭代器之外,一个高层类别的迭代器支持低层类别迭代器的所有操作。C++ 标准指明了泛型和数值算法的每个迭代器参数的最小类别。

    对于向一个算法传递错误类别的迭代器问题,很多编译器不会给出任何警告或提示。

  • alg ( beg, end, other args)
    alg ( beg, end, dest, other args)
    alg ( beg, end, beg2, other args)
    alg ( beg, end, beg2, end2, other args)

    向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据;接受单独 beg2 的算法假定从 beg2 开始的序列与 beg 和 end 所表示的范围至少一样大。

  • 除了参数规范,算法还遵循一套命名和重载规范。函数的一个版本用元素类型的运算符来比较元素;另一个版本接受一个版外谓词参数,来代替 < 或 ==。接受谓词参数的算法都是附加的 _if 前缀的。

    默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中。这些算法还提供另一个版本,将元素写到一个指定的输出目的位置。

10.6 特定容器算法

  • 链表类型定义的其他算法的通用版本可以用于链表,但代价太高。这些算法需要交换输入序列中的元素。对于 list 和 forward_list,应该优先使用成员函数版本的算法而不是通用算法。

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

待完善…

0 0
原创粉丝点击