C++ 之 高效使用STL(泛型算法设计原理解析)

来源:互联网 发布:阿里年会马云跳舞 编辑:程序博客网 时间:2024/06/05 19:05

C++ 之 模板与泛型编程(四、泛型算法) 

 

标准库容器定义的操作非常少。标准库没有给容器添加大量的功能函数,而是选择提供一组算法,这些算法大都不依赖特定的容器类型,是“泛型”的,可作用在不同类型的容器和不同类型的元素上。

 

标准容器(the standard container)定义了很少的操作。大部分容器都支持添加和删除元素;访问第一个和最后一个元素;获取容器的大小,并在某些情况下重设容器的大小;以及获取指向第一个元素和最后一个元素的下一位位置的迭代器。

 

因为它们实现共同的操作,所以称之为“算法”;而“泛型”指的是它们可以操作在多种容器类型上——不但可作用于 vector 或 list 这些标准库类型,还可用在内置数组类型、甚至其他类型的序列上,这些我们将在本章的后续内容中了解。自定义的容器类型只要与标准库兼容,同样可以使用这些泛型算法。

 

大多数算法是通过遍历由两个迭代器标记的一段元素来实现其功能。典型情况下,算法在遍历一段元素范围时,操纵其中的每一个元素。算法通过迭代器访问元素,这些迭代器标记了要遍历的元素范围。

 

标准算法固有地独立于类型

这种算法只在一点上隐式地依赖元素类型:必须能够对元素做比较运算。该算法的明确要求如下:

  1. 需要某种遍历集合的方式:能够从一个元素向前移到下一个元素。

  2. 必须能够知道是否到达了集合的末尾。

  3. 必须能够对容器中的每一个元素与被查找的元素进行比较。

  4. 需要一个类型指出元素在容器中的位置,或者表示找不到该元素。

迭代器将算法和容器绑定起来

 

泛型算法用迭代器来解决第一个要求:遍历容器

 

第一个迭代器==第二个迭代器 就可以知道是到达了集合的末尾

 

使用超出末端迭代器还可以很方便地处理第四个要求,只要以此迭代器为返回值,即可表示没有找到要查找的元素。如果要查找的值未找到,则返回超出末端迭代器;否则,返回的迭代器指向匹配的元素。

 

第三个要求——元素值的比较,有两种解决方法。默认情况下,find 操作要元素类型定义了相等(==)操作符,算法使用这个操作符比较元素。如果元素类型不支持相等(==)操作符,或者打算用不同的测试方法来比较元素,则可使用第二个版本的 find 函数。这个版本需要一个额外的参数:实现元素比较的函数名字。

 iterator find( iterator start, iterator end, const TYPE& val );

 iterator find_if( iterator start, iterator end, UnPred up );

 

标准库提供了超过 100 种算法。与容器一样,算法有着一致的结构。比起死记全部一百多种算法,了解算法的设计可使我们更容易学习和使用它们。本章除了举例说明这些算法的使用之外,还将描述标准库算法的统一原理。

 

泛型算法本身从不执行容器操作,只是单独依赖迭代器和迭代器操作实现。算法基于迭代器及其操作实现,而并非基于容器操作。这个事实也许比较意外,但本质上暗示了:使用“普通”的迭代器时,算法从不修改基础容器的大小。正如我们所看到的,算法也许会改变存储在容器中的元素的值,也许会在容器内移动元素,但是,算法从不直接添加或删除元素。

 

通常,泛型算法都是在标记容器(或其他序列)内的元素范围的迭代器上操作的。标记范围的两个实参类型必须精确匹配,而迭代器本身必须标记一个范围:它们必须指向同一个容器中的元素(或者超出容器末端的下一位置),并且如果两者不相等,则第一个迭代器通过不断地自增,必须可以到达第二个迭代器。

 

确保算法有足够的元素存储输出数据的一种方法是使用插入迭代器。插入迭代器是可以给基础容器添加元素的迭代器。通常,用迭代器给容器元素赋值时,被赋值的是迭代器所指向的元素。而使用插入迭代器赋值时,则会在容器中添加一个新元素,其值等于赋值运算的右操作数的值。

 

在研究算法标准库的结构之前,先看一些例子。上一节已经介绍了 find 函数的用法;本节将要使用其他的一些算法。使用泛型算法必须包含 algorithm 头文件:

    #include <algorithm>
标准库还定义了一组泛化的算术算法(generalized numeric algorithm),其命名习惯与泛型算法相同。使用这些算法则
必须包含 numeric 头文件:
    #include <numeric>

 

除了少数例外情况,所有算法都在一段范围内的元素上操作,我们将这段范围称为“输出范围(input range)”。带有输入范围参数的算法总是使用头两个形参标记该范围。这两个形参是分别指向要处理的第一个元素和最后一个元素的下一位置的迭代器。

 

尽管大多数算法对算法对输入范围的操作是类似的,但在该范围内如何操纵元素却有所不同。理解算法的最基本方法是了解该算法是否读元素、写元素或者对元素进行重新排序。

算法最基本的性质是需要使用的迭代器种类。所有算法都指定了它的每个迭代器形参可使用的迭代器类型。如果形参必须为随机访问迭代器则可提供 vectordeque 类型的迭代器,或者提供指向数组的指针。而其他容器的迭代器不能用在这类算法上。

C++ 还提供了另外两种算法模式:一种模式由算法所带的形参定义;另一种模式则通过两种函数命名和重载的规范定义。

 

算法的形参模式

任何其他的算法分类都含有一组形参规范。理解这些形参规范有利于学习新的算法——只要知道形参的含义,就可专注于了解算法实现的操作。大多数算法采用下面四种形式之一:

      alg (beg, end, other parms);     alg (beg, end, dest, other parms);     alg (beg, end, beg2, other parms);     alg (beg, end, beg2, end2, other parms);

其中,alg 是算法的名字,begend 指定算法操作的元素范围。我们通常将该范围称为算法的“输入范围”。尽管几乎所有算法都有输入范围,但算法是否使用其他形参取决于它所执行的操作。这里列出了比较常用的其他形参:destbeg2end2,它们都是迭代器。这些迭代器在使用时,充当类似的角色。除了这些迭代器形参之外,有些算法还带有其他的菲迭代器形参,它们是这些算法特有的。

 

带有单个目标迭代器的算法

dest 形参是一个迭代器,用于指定存储输出数据的目标对象。算法假定无论需要写入多少个元素都是安全的。

 

注意:调用这些算法时,必须确保输出容器有足够大的容量存储输出数据,这正是通常要使用插入迭代器或者 ostream_iterator 来调用这些算法的原因。如果使用容器迭代器调用这些算法,算法将假定容器里有足够多个需要的元素。

 

如果 dest 是容器上的迭代器,则算法将输出内容写到容器中已存在的元素上。更普遍的用法是,将 dest 与某个插入迭代器或者 ostream_iterator 绑定在一起。插入迭代器在容器中添加元素,以确保容器有足够的空间存储输出。ostream_iterator 则实现写输出流的功能,无需要考虑所写的元素个数。

 

带第二个输入序列的算法

有一些算法带有一个 beg2 迭代器形参,或者同时带有 beg2end2 迭代器形参,来指定它的第二个输入范围。这类算法通常将联合两个输入范围的元素来完成计算功能。算法同时使用 beg2end2 时,这些迭代器用于标记完整的第二个范围。也就是说,此时,算法完整地指定了两个范围:begend 标记第一个输入范围,而 beg2end2 则标记第二个输入范围。

带有 beg2 而不带 end2 的算法将 beg2 视为第二个输入范围的首元素,但没有指定该范围的最后一个元素。这些算法假定以 beg2 开始的范围至少与 begend 指定的范围一样大。

 

算法的命名规范

标准库使用一组相同的命名和重载规范,了解这些规范有助于更容易地学习标准库。它们包括两种重要模式:第一种模式包括测试输入范围内元素的算法,第二种模式则应用于对输入范围内元素重新排序的算法。

 

区别带有一个值或一个谓词函数参数的算法版本

很多算法通过检查其输入范围内的元素实现其功能。这些算法通常要用到标准关系操作符:==<。其中的大部分算法会提供第二个版本的函数,允许程序员提供比较或测试函数取代操作符的使用。

 

重新对容器元素排序的算法要使用 < 操作符。这些算法的第二个重载版本带有一个额外的形参,表示用于元素排序的不同运算:

      sort (beg, end);         // use < operator to sort the elements     sort (beg, end, comp);   // use function named comp to sort the elements

 

检查指定值的算法默认使用 == 操作符。系统为这类算法提供另外命名的(而非重载的)版本,带有谓词函数形参。带有谓词函数形参的算法,其名字带有后缀 _if

           find(beg, end, val);       // find first instance of val in the input range
           find_if(beg, end, pred);   // find first instance for which pred is true

区别是否实现复制的算法版本

无论算法是否检查它的元素值,都可能重新排列输入范围内的元素。在默认情况下,这些算法将重新排列的元素写回其输入范围。标准库也为这些算法提供另外命名的版本,将元素写到指定的输出目标。此版本的算法在名字中添加了 _copy 后缀:

      reverse(beg, end);     reverse_copy(beg, end, dest);

 

容器特有的算法

     

 

再谈迭代器

C++ 语言还提供了另外三种迭代器:

 

 

1、插入迭代器:这类迭代器与容器绑定在一起,实现在容器中插入元素的功能。

 

2、iostream 迭代器:这类迭代器可与输入或输出流绑

定在一起,用于迭代遍历所关联的 IO 流

 

3、反向迭代器:这类迭代器实现向后遍历,而不是向前遍历。所有容器类型都定义了自己的 reverse_iterator 类型,由 rbeginrend 成员函数返回。

 

上述迭代器类型都在 iterator 头文件中定义。

 

back_inserter 函数是一种插入器。插入器是一种迭代器适配器(第 9.7 节),带有一个容器参数,并生成一个迭代器,用于在指定容器中插入元素。通过插入迭代器赋值时,迭代器将会插入一个新的元素。C++ 语言提供了三种插入器,其差别在于插入元素的位置不同。

 

back_inserter,创建使用 push_back 实现插入的迭代器。

front_inserter,使用 push_front 实现插入。

inserter,使用 insert 实现插入操作。除了所关联的容器外,inserter 还带有第二实参:指向插入起始位置的迭代器。

 

inserter 适配器提供更普通的插入形式。这种适配器带有两个实参:所关联的容器和指示起始插入位置的迭代器。

 

 

原创粉丝点击