STL是一种泛型编程。面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但它们的理念决然不同。
泛型编程旨在编写独立于数据类型的代码。在C++中,完成通用程序的工具是模板。模板使得能够按泛型定义函数或类,而STL通过通用算法更近了一步。模板让这一切成为可能,但必须对元素进行仔细的设计。
为何使用迭代器
理解迭代器是理解STL的关键所在。模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类。因此,它们都是STL通用方法的重要组成部分。
下面我们来看如何为两种不同数据表示实现find函数。
在double数组中搜索特定值的函数。
double * find_ar(double * ar, int n, const double & val)
{
for (int i = 0; i < n; i++)
if (ar[i] == val)
return &ar[i];
return 0;
}
可以用模板将这种算法推广到包含==运算符、任意类型的数组。尽管如此,这种算法仍然与一种特定的数据结构(数组)关联在一起。
下面是搜索另一种数据结构——链表的情况。
struct Node
{
double item;
Node * p_next;
}
Node * find_ll(Node * head, const double & val)
{
Node * start;
for (start = head; start != 0; start = start->p_next)
if (start->item == val)
return start;
return 0;
}
同样,可以使用模板将这种算法推广到支持==运算符的任何数据类型的链表。然而,这种算法也是与特定的数据结构——链表关联在一起。
从实现细节上看,这两个find函数的算法是不同的:一个使用数组索引来遍历元素,另一个则将start重置为start->p_next。但从广义上来说,,这两种算法是相同的:将值依次与容器中的每个值进行比较,直到找到匹配的为止。
泛型编程旨在使用同一个find函数来处理数组、链表或任何其他容器类型。即函数不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构。模板提供了存储在容器中的数据类型的通用表示,因此还需要遍历容器中的值的通用表示,迭代器正是这样的通用表示。
对于要实现find函数,迭代器应具备哪些特征呢?
• 应能够对迭代器执行解除引用的操作;
• 应能够将一个迭代器赋给另一个;
• 应能够将一个迭代器与另一个进行比较,看他们是否相等。
• 应能够使用迭代器遍历容器中的所有元素,这可以通过为迭代器p定义++p和p++来实现。
迭代器也可以完成其他的操作。实际上STL按功能的强弱定义了多种级别的迭代器。
对于find_ar()函数使用常规指针就能满足迭代器的要求
typedef double * iterator;
iterator find_ar(iterator begin, iterator end, const double & val)
{
iterator ar;
for (ar = begin; ar != end; ar++)
if (*ar == val)
return ar;
return end;
}
对于find_ll函数,可以定义一个迭代器类,
class iterator
{
struct Node
{
double item;
Node * p_next;
};
Node * pt;
public:
iterator() : pt(0) {}
iterator(Node * pn) : pt(pn) {}
double operator*() {return pt->item;}
iterator & operator++()
{
pt = pt->p_next;
return *this;
}
iterator operator++(int)
{
iterator tmp = *this;
pt = pt->p_next;
return tmp;
}
...
}
有了这样的iterator类之后,第二个find函数就可以这样编写:
iterator find_ll(iterator head, const double & val)
{
iterator start;
for (start = head; start != 0; ++start)
if (*start == val)
return start;
return 0;
}
这和find_ar几乎相同,唯一的区别是find_ar函数使用超尾迭代器,而find_ll使用存储在最后一个节点中的空值。可以让链表的最后一个元素后面还有一个额外的元素,即让数组和链表都有超尾元素。这样两个函数检测数据的方式将相同,从而成为相同的算法。注意,增加超尾元素后,对迭代器的要求变成了对容器类的要求。
STL遵循上面介绍的方法。首先,每个容器类定义了相应的迭代器类型。对于其中的某个类,迭代器可能是指针;而对于另一个类,则可能是对象。不管实现方式如何,迭代器都将提供所需的操作,如*和++.其次,每个容器类都有一个超尾标记,当迭代器递增到超越容器的最后一个值后,这个值将被赋给迭代器。每个容器类都有begin()和end()方法,它们分别返回一个指向容器的第一个元素和超尾位置的迭代器。
迭代器类型
STL定义了5种迭代器,并根据所需的迭代器类型对算法进行了描述。这5种迭代器分别是输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器。
//输入迭代器
template<class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& value);
//随机访问迭代器
template<class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last);
1.输入迭代器
术语“输入”是从程序的角度说的,即来自容器的信息被视为输入。具体的说,对输入迭代器解除引用将使程序能够读取容器中的值,但不一定能让程序修改值。因此,需要输入迭代器的算法不会修改容器中的值。并不能保证输入迭代器第二次遍历容器时,顺序不变。另外,输入迭代器被递增后,也不能保证其先前的值仍然可以被解除引用。基于输入迭代器的任何算法都应当是单通行的,不依赖与前一次遍历是的迭代器值,也不依赖于本次遍历中前面的迭代器值。
注意,输入迭代器是单向迭代器,可以递增,但不能倒退。
2.输出迭代器
术语“输出”来指用于将信息从程序传输给容器的迭代器,因此程序的输出就是容器的输入。输出迭代器和输入迭代器相似,只是解除引用让程序能修改容器值,而不能读取。比如发送到显示器上的输出就是如此,cout可以修改发送到显示器的字符流,却不能读取屏幕上的内容。
简而言之,对于单通行、只读算法,可以使用输入迭代器;而对于单通行、只写算法,则可以使用输出迭代器。
3.正向迭代器
与输入和输出迭代器相似,正向迭代器只使用++运算符来遍历容器,所以它每次沿容器向前移动一个元素;然而它与输入和输出迭代器不同的是,它总是按相同的顺序遍历一系列值。另外,正向迭代器递增后,仍然可以对前面的迭代器值解除引用(如果保存了它),并可以得到相同的值。这些特征使得多次通行算法成为可能。
正向迭代器既可以使得能够读取和修改数据,也可以使得只能读取数据。
4.双向迭代器
双向迭代器具有正向迭代器的所有特性,同时支持两种(前缀和后缀)递减运算符。
5.随机访问迭代器
有些算法要求能够直接跳到容器中的任何一个元素,这叫随机访问,需要随机访问迭代器。随机访问迭代器具有双向迭代器的所有特性,同时添加了支持随机访问的操作和用于对元素进行排序的关系运算符。
迭代器层次结构
正向迭代器具有输入和输出迭代器的全部功能,同时拥有自己的功能;双向迭代器具有正向迭代器的全部功能,同时还有自己的功能;随机访问迭代器具有正向迭代器的全部功能,同时还有自己的功能。
迭代器功能
迭代器功能 | 输入 | 输出 | 正向 | 双向 | 随机访问 |
---|---|---|---|---|---|
解除引用读取 | 有 | 无 | 有 | 有 | 有 |
解除引用写入 | 无 | 有 | 有 | 有 | 有 |
固定可重复排序 | 无 | 无 | 有 | 有 | 有 |
++i i++ | 有 | 有 | 有 | 有 | 有 |
–i i– | 无 | 无 | 无 | 有 | 有 |
i[n] | 无 | 无 | 无 | 无 | 有 |
i+n | 无 | 无 | 无 | 无 | 有 |
i-n | 无 | 无 | 无 | 无 | 有 |
i+=n | 无 | 无 | 无 | 无 | 有 |
i-=n | 无 | 无 | 无 | 无 | 有 |
根据特定迭代器类型编写的算法可以使用该种迭代器,也可以使用具有所需功能的任何其它迭代器。所以具有随机访问迭代器的容器可以使用为输入迭代器编写的算法。有这么多迭代器的目的是为了在编写算法尽可能使用要求最低的迭代器,并让它适用于容器的最大区间。
每个容器类都定义了一个类型typedef名称——iterator,因此vector类的迭代器类型为vector::interator,然而矢量迭代器是随机访问迭代器,它允许使用基于任何迭代器类型的算法。同样list::interator。STL实现了一个双向链表,它使用双向迭代器,因此不能使用基于随机访问迭代器的算法,但可以使用基于要求较低的迭代器的算法。