2.7 vector简介

使用string,我们可以向string对象输入数据而不关心需要多少存储空间。但如果把每一行读入一个string对象,我们就不知道需要多少string—只有读完整个文件后才知道。为了解决这一问题,我们需要有某种能够自动扩展的存放设施,用以包含所需数量的string对象。

实际上,为什么要限制我们自己只存放string对象呢?当编写程序时,很多情况下并不知道会用到多少什么东西。如果有某种“容器”对象,它能容纳所有的各种对象,这似乎更有用。幸运的是,标准C++库有一个现成的解决方法:标准容器(container)类。容器类是标准C++非常实用的强大工具之一。

人们经常会把标准C++库的“容器”与“算法”和被称为STL的东西相混淆。STL(标准模板类库,Standard T emplate Library)是1994年春天Alex Stepanov在加州San Diego的会议上把他的C++库提交给C++标准委员会时使用的名称(Alex Stepanov当时在惠普公司工作)。这个名称一直沿用下来,特别是惠普决定允许这个库公开下载后,使用的人就更多了。同时,C++标准委员会对STL作了大量的修改,将它整合进标准C++类库。SGI公司(参见http://www.sgi.com/Technology/STL)不断对STL进行改进。SGI的STL与标准的C++库在许多细节上是不同的。虽然人们经常产生误解,但实际上C++标准是不“包括”STL的。由于标准C++库的“容器”和“算法”与SGI的STL有相同的来源(通常同名),因此容易引起误会。所以,本书中,将使用“标准C++库”或“标准库容器”或其他类似的说法,避免使用“STL”这个术语。

虽然标准C++库容器和算法的实现所使用的某些概念较深奥,并且在本书第2卷中专门用了两章来讲解这些概念,但即使对这些概念不太了解,也不妨碍这些库的使用。最基本的标准容器—“vector”非常有用,在这里对它作一些介绍,以后会经常用到。我们会发现,使用vector后,可以进行大量的工作而不用关心底层的实现(再强调一下,这就是面向对象编程的一个重要目标)。当读完第2卷中有关标准类库的章节后,我们会学到更多的关于vector和其他容器的知识。如果本书较早的程序中使用vector并不像有经验的C++程序员所做的那样,这是可以理解的。一般说来,这里的多数用法还是适当的。

vector类是一个模板(template),也就是说它可有效地用于不同的类型。就是说,我们可以创建Shape的vector、Cat的vector和String的vector等。用模板几乎可以创建“任何事物的类”。把类型名输入到尖括号内,让编译器知道vector所用的类(在这种情况下就是vector将要保存的类)。所以,string的vector表示为vector<string>。这样,就定制了只装string对象的vector。如果试图在这个vector中加入其他类型,编译器会给出错误提示信息。

既然vector表达了“容器”的概念,就应该有一定的方法把东西放进容器中,并且能从容器里把东西取出来。为了在vector末尾后追加一个新元素,可以使用成员函数push_back()(注意,对于一个具体的对象要用“.”号来调用它的成员函数)。“push_back()”这个名字看上去似乎有些冗长,不如“put”简单,这样命名是因为还有别的容器和成员函数也要向容器添加新元素。例如,insert()成员函数,它是在容器中间加入新元素,vector支持这个函数,但它的用法更复杂,第2卷再解释它。还有push_front()函数(不属于vector),它是把新元素加到vector的开头。在vector中,还有很多成员函数,在标准的C++类库中,还有很多容器,但是令人惊奇的是,仅仅知道一些简单的特征就能做许多事情了。

可以用push_back()向vector内添加新元素,但怎样从vector取回这些元素呢?解决的方法很巧妙—操作符重载,让vector像数组那样使用。几乎每一种编程语言都有数组这种数据类型(下一章将对它作更多的讨论)。数组是一个集合体,即它由许多元素构成。数组的一个显著特点是它所有的元素大小相同且逐个邻接。最重要的是元素可由“下标”(indexing)选定,这意味着,只要说“我要第n个元素”,就能找到这个元素,通常很快。除了某些特例,一般的编程语言下标都用方括号表示。比如,对于一个数组a,想提出第5个单元,就可以写成a[4](注意下标总是从0开始)。

正如“<<”和“>>”可用于iostreams类一样,通过操作符重载也可把简单有效的下标记号用于vector类中。不必知道重载是如何实现的—它留到下一章讨论—但是,如果知道为了使[]与vector一起操作而隐藏了的某些技巧,这对于加深理解是有帮助的。

了解了上述内容,现在来看一个使用vector的程序。为使用vector,必须包含头文件<vector>:

2.7 vector简介 - 图1

2.7 vector简介 - 图2

程序大部分与前一个程序相同,打开文件并每次将一行读进string对象。不同的是,这些string对象被压入vector v的尾部。while循环完成时,整个文件存在于v内,并驻留内存。

while语句之后是for循环语句。它与while语句相似,不过它多了一些控制条件。for之后的括号内是控制表达式,这和while语句相同。但它有一个在括号内的控制表达式,由三部分组成:第一部分初始化;第二部分检测退出循环的条件;第三部分改变某些内容,通常是为了遍历一个数据项序列。程序中的这种for循环方式是非常通行的用法:初始化部分int i=0表示用一个整数i作循环计数器,并初始化为0;检测部分表明,要使循环继续,i的值必须小于vector对象v中的元素个数(元素个数由成员函数size()得出);最后一部分用到了C/C++中的自增操作符,使i加1。确切地说,i++表示取i的值加上1,并把结果返回给i。所以,整个for循环就是取控制变量i,使它从0逐渐递增至比vector对象的个数小1时结束。对于i的每一个值,执行一次cout语句,建立一行,它由i的值(由cout转化为字符数组)、分号、空格、文件中的一行句和由endl产生的一个换行符组成。编译和运行这个程序,可以看出其结果是给文件加上了行号。

因为在iostreams中能使用操作符“>>”,所以可以很容易地修改上面的程序,使之把输入分解成由空格分隔的单词而不是一些行。

2.7 vector简介 - 图3

表达式:

2.7 vector简介 - 图4

意思是每次取输入的一个单词,当表达式的值为“false”时,就意味着文件读完了。当然,以空白来分隔单词是比较原始的办法,这里只是举一个简单例子。后面,会看到更复杂的例子,它们可以根据任何方式分割输入。

为了进一步说明使用可带任何类型的vector是很容易的事,下面给出一个创建vector<int>的例子:

2.7 vector简介 - 图5

创建可以存放不同类型的vector,只需把类型当做模板参数(即在尖括号中的参数)输入即可。提供模板和设计完善的模板库正是为了使这种使用变得容易。

在这个例子中,我们还可以看到vector的另外一个重要特征。在表达式

2.7 vector简介 - 图6

中可以看到,vector不仅仅限于输入和取出,还可以通过使用方括号的下标操作符向vector的任何一个单元赋值(从而改变单元的值)。这说明vector是通用、灵活的“暂存器”,用来处理对象集。在后面几章我们将充分地利用它。