c++ primer plus 第16章string 类和标准模板库, 泛型编程----为何使用迭代器
c++ primer plus 第16章string 类和标准模板库, 泛型编程----为何使用迭代器
泛型编程16.4
有了一些使用 STL的经验后,来看一看底层理念。STL是一种泛型编程(generic programming)。面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但它们的理念绝然不同。
泛型编程旨在编写独立于数据类型的代码。在 C++中,完成通用程序的工具是模板。当然,模板使得能够按泛型定义函数或类,而 STL通过通用算法更进了一步。模板让这一切成为可能,但必须对元素进行仔细地设计。为解模板和设计是如何协同工作的,来看一看需要迭代器的原因。
16.4.1 为何使用迭代器
理解迭代器是理解 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;//or,in C++1l,return nullptr;
}
如果函数在数组中找到这样的值,则返回该值在数组中的地址,否则返回一个空指针。该函数使用下标来遍历数组。可以用模板将这种算法推广到包含==运算符的、任意类型的数组。尽管如此,这种算法仍然与一种特定的数据结构(数组)关联在一起。
下面来看搜索另一种数据结构–链表的情况(第12章使用链表实现了Queue类)。链表由链接在一起的 Node 结构组成:
struct Node
{
double item;
Node *p_next;
}
假设有一个指向链表第一个节点的指针,每个节点的pnext指针都指向下一个节点,链表最后一个节点的pnext指针被设置为0,则可以这样编写findll()函数:
Node* find 1l(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->pnext。但从广义上说,这两种算法是相同的:将值依次与容器中的每个值进行比较,直到找到匹配的为止。
泛型编程旨在使用同一个find函数来处理数组、链表或任何其他容器类型。即函数不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构。模板提供了存储在容器中的数据类型的通用表示,因此还需要遍历容器中的值的通用表示,迭代器正是这样的通用表示。
要实现 fnd 函数,迭代器应具备哪些特征呢?下面是一个简短的列表。
- 应能够对迭代器执行解除引用的操作,以便能够访问它引用的值。即如果p是一个迭代器,则应对*p 进行定义。
- 应能够将一个迭代器赋给另一个。即如果p和q都是迭代器,则应对表达式p=q进行定义。
- 应能够将一个迭代器与另一个进行比较,看它们是否相等。即如果p和q都是迭代器,则应对p= =q和 p!=q 进行定义。
- 应能够使用迭代器遍历容器中的所有元素,这可以通过为迭代器p定义++p和p++来实现。迭代器也可以完成其他的操作,但有上述功能就足够了,至少对于find 函数是如此。实际上,STL按功能的强弱定义了多种级别的迭代器,这将在后面介绍。顺便说一句,常规指针就能满足迭代器的要求,因此,可以这样重新编写findarr()函数:
typedef double * iterator;
iterator find_ar(iterator ar,int n,const double & val)
{
for(int i=0;i<n; i++,ar++)
if (*ar == val)
return ar;
return 0;
}
然后可以修改函数参数,使之接受两个指示区间的指针参数,其中的一个指向数组的起始位置,另个指向数组的超尾(程序清单7.8与此类似);同时函数可以通过返回尾指针,来指出没有找到要找的值。
下面的 fnd 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;//indicates val not found
}
对于 find 11()函数,可以定义一个迭代器类,其中定义了运算符*和++:
struct Node
{
double item;
Node *p_next;
};
class iterator
{
Node *pt;
public:
iterator():pt(0){}
iterator(Node*pn):pt(pn){}
double operator*(){return pt->item;}
iterator& operator++()// for ++it
{
pt = pt->p next;
}
return *this;
iterator operator++(int)// for it++
{
iterator tmp =*this;
pt = pt->p_next;
return tmp;
}
//... operator==(),operator!=(),etc.
};
为区分++运算符的前缀版本和后缀版本,C++将operator++作为前缀版本,将operator++(imnt)作为后缀版本;其中的参数永远也不会被用到,所以不必指定其名称。这里重点不是如何定义 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 1( )使用存储在最后一个节点中的空值。除了这种差别外,这两个函数完全相同。例如,可以要求链表的最后一个元素后面还有一个额外的元素,即让数组和链表都有超尾元素,并在迭代器到达超尾位置时结束搜索。这样,find ar()和 find 1()检测数据尾的方式将相同,从而成为相同的算法。注意,增加超尾
元素后,对迭代器的要求变成了对容器类的要求。STL,遵循上面介绍的方法。首先,每个容器类(vector、list、deque 等)定义了相应的迭代器类型。对于其中的某个类,迭代器可能是指针;而对于另一个类,则可能是对象。不管实现方式如何,迭代器都将提供所需的操作,如*和++(有些类需要的操作可能比其他类多)。其次,每个容器类都有一个超尾标记,当迭代器递增到超越容器的最后一个值后,这个值将被赋给迭代器。每个容器类都有 begin()和 end()方法,它们分别返回一个指向容器的第一个元素和超尾位置的迭代器。每个容器类都使用++操作,让迭代器从指
向第一个元素逐步指向超尾位置,从而遍历容器中的每一个元素。使用容器类时,无需知道其迭代器是如何实现的,也无需知道超尾是如何实现的,而只需知道它有迭代器,其 begin()返回一个指向第一个元素的迭代器,end()返回一个指向超尾位置的迭代器即可。例如,假设要打印 vector对象中的值,则可以这样做:
vector<double>::iterator pr;
for(pr=scores.begin();pr != scores.end();pr++)
Cout << *pr << endl
其中,下面的代码行将pr的类型声明为vector类的迭代器:
vector<double>class:
vector<double>::iterator pr;
如果要使用 list类模板来存储分数,则代码如下:
list<double>::iterator pr;
for(pr =scores.begin();pr != scores.end();pr++)
Cout << *pr << endl;
唯一不同的是 pr 的类型。因此,STL通过为每个类定义适当的迭代器,并以统一的风格设计类,能够对内部表示绝然不同的容器,编写相同的代码。
使用C++11新增的自动类型推断可进一步简化:对于矢量或列表,都可使用如下代码:
for(auto pr=scores.begin();pr != scores.end(); pr++)
cout <<*pr <<endli
实际上,作为一种编程风格,最好避免直接使用迭代器,而应尽可能使用STL函数(如foreach()来处理细节。也可使用C++11新增的基于范围的for循环:
for(autox:scores)
cout<<x<<endl;
来总结一下 STL方法。首先是处理容器的算法,应尽可能用通用的术语来表达算法,使之独立于数据类型和容器类型。为使通用算法能够适用于具体情况,应定义能够满足算法需求的迭代器,并把要求加到容器设计上。即基于算法的要求,设计基本迭代器的特征和容器特征。