stl 学习(转帖)

C++ STL轻松导学
 名称 C++ STL轻松导学
 作者 晨光(Morning)
 简介 本教程介绍有关学习C++ STL的预备知识和STL的相关背景知识,适合想对STL做大致了解的初学者。
 声明 本教程版权为晨光(Morning)所有,未经允许,请勿复制、传播,谢谢。

 目录

1 初识STL:解答一些疑问 2 牛刀小试:且看一个简单例程


作为C++标准不可缺少的一部分,STL应该是渗透在C++程序的角角落落里的。STL不是实验室里的宠儿,也不是程序员桌上的摆设,她的激动人心并非昙花一现。本教程旨在传播和普及STL的基础知识,若能借此机会为STL的推广做些力所能及的事情,到也是件让人愉快的事情。


1 初识STL:解答一些疑问1.1 一个最关心的问题:什么是STL1.2 追根溯源:STL的历史1.3 千丝万缕的联系1.3.1 STL和C++1.3.2 STL和C++标准函数库1.3.3 STL和GP,GP和OOP1.4 STL的不同实现版本1.4.1 HP STL1.4.2 P.J. Plauger STLhttp://www.dinkumware.com。据称Visual Studio.NET中的Visual C++.NET(即VC7.0),对C++标准的支持有所提高,并且多了以哈希表(hash table)为基础而实现的map容器,multimap容器和set容器。

"什么是STL?",假如你对STL还知之甚少,那么我想,你一定很想知道这个问题的答案,坦率地讲,要指望用短短数言将这个问题阐述清楚,也决非易事。因此,如果你在看完本节之后还是觉得似懂非懂,大可不必着急,在阅读了后续内容之后,相信你对STL的认识,将会愈加清晰、准确和完整。不过,上述这番话听起来是否有点像是在为自己糟糕的表达能力开脱罪责呢?:)

不知道你是否有过这样的经历。在你准备着手完成数据结构老师所布置的家庭作业时,或者在你为你所负责的某个软件项目中添加一项新功能时,你发现需要用到一个链表(List)或者是映射表(Map)之类的东西,但是手头并没有现成的代码。于是在你开始正式考虑程序功能之前,手工实现List或者Map是不可避免的。于是……,最终你顺利完成了任务。或许此时,作为一个具有较高素养的程序员的你还不肯罢休(或者是一个喜欢偷懒的优等生:),因为你会想到,如果以后还遇到这样的情况怎么办?没有必要再做一遍同样的事情吧!

如果说上述这种情形每天都在发生,或许有点夸张。但是,如果说整个软件领域里,数十年来确实都在为了一个目标而奋斗--可复用性(reusability),这看起来似乎并不夸张。从最早的面向过程的函数库,到面向对象的程序设计思想,到各种组件技术(如:COM、EJB),到设计模式(design pattern)等等。而STL也在做着类似的事情,同时在它背后蕴涵着一种新的程序设计思想--泛型化设计(generic programming)。

继续上面提到的那个例子,假如你把List或者map完好的保留了下来,正在暗自得意。且慢,如果下一回的List里放的不是浮点数而是整数呢?如果你所实现的Map在效率上总是令你不太满意并且有时还会出些bug呢?你该如何面对这些问题?使用STL是一个不错的选择,确实如此,STL可以漂亮地解决上面提到的这些问题,尽管你还可以寻求其他方法。

说了半天,到底STL是什么东西呢?

STL(Standard Template Library),即标准模板库,是一个具有工业强度的,高效的C++程序库。它被容纳于C++标准程序库(C++ Standard Library)中,是ANSI/ISO C++标准中最新的也是极具革命性的一部分。该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。为广大C++程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。这种现象有些类似于Microsoft Visual C++中的MFC(Microsoft Foundation Class Library),或者是Borland C++ Builder中的VCL(Visual Component Library),对于此二者,大家一定不会陌生吧。

从逻辑层次来看,在STL中体现了泛型化程序设计的思想(generic programming),引入了诸多新的名词,比如像需求(requirements),概念(concept),模型(model),容器(container),算法(algorithmn),迭代子(iterator)等。与OOP(object-oriented programming)中的多态(polymorphism)一样,泛型也是一种软件的复用技术。

从实现层次看,整个STL是以一种类型参数化(type parameterized)的方式实现的,这种方式基于一个在早先C++标准中没有出现的语言特性--模板(template)。如果查阅任何一个版本的STL源代码,你就会发现,模板作为构成整个STL的基石是一件千真万确的事情。除此之外,还有许多C++的新特性为STL的实现提供了方便。

不知你对这里一下子冒出这么多术语做何感想,希望不会另你不愉快。假如你对它们之中的大多数不甚了解,敬请放心,在后续内容中将会对这些名词逐一论述。正如开头所提到的。

有趣的是,对于STL还有另外一种解释--STepanov & Lee,前者是指Alexander Stepanov,STL的创始人;而后者是Meng Lee,她也是使STL得以推行的功臣,第一个STL成品就是他们合作完成的。这一提法源自1995年3月,Dr.Dobb's Journal特约记者, 著名技术书籍作家Al Stevens对Alexander Stepanov的一篇专访。

在结识新朋友的时候,大多数人总是忍不住想了解对方的过去。本节将带您简单回顾一下STL的过去。

被誉为STL之父的Alexander Stepanov,出生于苏联莫斯科,早在20世纪70年代后半期,他便已经开始考虑,在保证效率的前提下,将算法从诸多具体应用之中抽象出来的可能性,这便是后来泛型化思想的雏形。为了验证自己的思想,他和纽约州立大学教授Deepak Kapur,伦塞里尔技术学院教授David Musser共同开发了一种叫做Tecton的语言。尽管这次尝试最终没有取得实用性的成果,但却给了Stepanov很大的启示。

在随后的几年中,他又和David Musser等人先后用Schema语言(一种Lisp语言的变种)和Ada语言建立了一些大型程序库。这其间,Alexander Stepanov开始意识到,在当时的面向对象程序设计思想中所存在的一些问题,比如抽象数据类型概念所存在的缺陷。Stepanov希望通过对软件领域中各组成部分的分类,逐渐形成一种软件设计的概念性框架。

1987年左右,在贝尔实验室工作的Alexander Stepanov开始首次采用C++语言进行泛型软件库的研究。但遗憾的是,当时的C++语言还没有引入模板(template)的语法,现在我们可以清楚的看到,模板概念之于STL实现,是何等重要。是时使然,采用继承机制是别无选择的。尽管如此,Stepanov还是开发出了一个庞大的算法库。与此同时,在与Andrew Koenig(前ISO C++标准化委员会主席)和Bjarne Stroustrup(C++语言的创始人)等顶级大师们的共事过程中,Stepanov开始注意到C/C++语言在实现其泛型思想方面所具有的潜在优势。就拿C/C++中的指针而言,它的灵活与高效运用,使后来的STL在实现泛型化的同时更是保持了高效率。另外,在STL中占据极其重要地位的迭代子概念便是源自于C/C++中原生指针( native pointer)的抽象。

1988年,Alexander Stepanov开始进入惠普的Palo Alto实验室工作,在随后的4年中,他从事的是有关磁盘驱动器方面的工作。直到1992年,由于参加并主持了实验室主任Bill Worley所建立的一个有关算法的研究项目,才使他重新回到了泛型化算法的研究工作上来。项目自建立之后,参与者从最初的8人逐渐减少,最后只剩下两个人--Stepanove本人和Meng Lee。经过长时间的努力,最终,信念与汗水所换来的是一个包含有大量数据结构和算法部件的庞大运行库。这便是现在的STL的雏形(同时也是STL的一个实现版本--HP STL)。

1993年,当时在贝尔实验室的Andrew Koenig看到了Stepanove的研究成果,很是兴奋。在他的鼓励与帮助下,Stepanove于是年9月的圣何塞为ANSI/ISO C++标准委员会做了一个相关演讲(题为"The Science of C++ Programming"),向委员们讲述了其观念。然后又于次年3月,在圣迭戈会议上,向委员会提交了一份建议书,以期使STL成为C++标准库的一部分。尽管这一建议十分庞大,以至于降低了被通过的可能性,但由于其所包含的新思想,投票结果以压倒多数的意见认为推迟对该建议的决定。

随后,在众人的帮助之下,包括Bjarne Stroustrup在内,Stepanove又对STL进行了改进。同时加入了一个封装内存模式信息的抽象模块,也就是现在STL中的allocator,它使STL的大部分实现都可以独立于具体的内存模式,从而独立于具体平台。在同年夏季的滑铁卢会议上,委员们以80%赞成,20%反对,最终通过了提案,决定将STL正式纳入C++标准化进程之中,随后STL便被放进了会议的工作文件中。自此,STL终于成为了C++家族中的重要一员。

此后,随着C++标准的不断改进,STL也在不断地作着相应的演化。直至1998年,ANSI/ISO C++标准正式定案,STL始终是C++标准中不可或缺的一大部件。

在你了解了STL的过去之后,一些名词开始不断在你的大脑中浮现,STL、C++、C++标准函数库、泛型程序设计、面向对象程序设计……,这些概念意味着什么?他们之间的关系又是什么?如果你想了解某些细节,这里也许有你希望得到的答案。

没有C++语言就没有STL,这么说毫不为过。一般而言,STL作为一个泛型化的数据结构和算法库,并不牵涉具体语言(当然,在C++里,它被称为STL)。也就是说,如果条件允许,用其他语言也可以实现之。这里所说的条件,主要是指类似于"模板"这样的语法机制。如果你没有略过前一节内容的话,应该可以看到,Alexander Stepanov在选择C++语言作为实现工具之前,早以采用过多种程序设计语言。但是,为什么最终还是C++幸运的承担了这个历史性任务呢?原因不仅在于前述那个条件,还在于C++在某些方面所表现出来的优越特性,比如:高效而灵活的指针。但是如果把C++作为一种OOP(Object-Oriented Programming,面向对象程序设计)语言来看待的话(事实上我们一般都是这么认为的,不是吗?),其功能强大的继承机制却没有给STL的实现帮上多大的忙。在STL的源代码里,并没有太多太复杂的继承关系。继承的思想,甚而面向对象的思想,还不足以实现类似STL这样的泛型库。C++只有在引入了"模板"之后,才直接导致了STL的诞生。这也正是为什么,用其他比C++更纯的面向对象语言无法实现泛型思想的一个重要原因。当然,事情总是在变化之中,像Java在这方面,就是一个很好的例子,jdk1.4中已经加入了泛型的特性。

此外,STL对于C++的发展,尤其是模板机制,也起到了促进作用。比如:模板函数的偏特化(template function partial specialization),它被用于在特定应用场合,为一般模板函数提供一系列特殊化版本。这一特性是继STL被ANSI/ISO C++标准委员会通过之后,在Bjarne和Stepanov共同商讨之下并由Bjarne向委员会提出建议的,最终该项建议被通过。这使得STL中的一些算法在处理特殊情形时可以选择非一般化的方式,从而保证了执行的效率。

STL是最新的C++标准函数库中的一个子集,这个庞大的子集占据了整个库的大约80%的分量。而作为在实现STL过程中扮演关键角色的模板则充斥了几乎整个C++标准函数库。在这里,我们有必要看一看C++标准函数库里包含了哪些内容,其中又有哪些是属于标准模板库(即STL)的。

C++标准函数库为C++程序员们提供了一个可扩展的基础性框架。我们从中可以获得极大的便利,同时也可以通过继承现有类,自己编制符合接口规范的容器、算法、迭代子等方式对之进行扩展。它大致包含了如下几个组件:

C标准函数库,基本保持了与原有C语言程序库的良好兼容,尽管有些微变化。人们总会忍不住留恋过去的美好岁月,如果你曾经是一个C程序员,对这一点一定体会颇深。或许有一点会让你觉得奇怪,那就是在C++标准库中存在两套C的函数库,一套是带有.h扩展名的(比如<stdio.h>),而另一套则没有(比如<cstdio>)。它们确实没有太大的不同。

语言支持(language support)部分,包含了一些标准类型的定义以及其他特性的定义,这些内容,被用于标准库的其他地方或是具体的应用程序中。

诊断(diagnostics)部分,提供了用于程序诊断和报错的功能,包含了异常处理(exception handling),断言(assertions),错误代码(error number codes)三种方式。

通用工具(general utilities)部分,这部分内容为C++标准库的其他部分提供支持,当然你也可以在自己的程序中调用相应功能。比如:动态内存管理工具,日期/时间处理工具。记住,这里的内容也已经被泛化了(即采用了模板机制)。

字符串(string)部分,用来代表和处理文本。它提供了足够丰富的功能。事实上,文本是一个string对象,它可以被看作是一个字符序列,字符类型可能是char,或者wchar_t等等。string可以被转换成char*类型,这样便可以和以前所写的C/C++代码和平共处了。因为那时侯除了char*,没有别的。

国际化(internationalization)部分,作为OOP特性之一的封装机制在这里扮演着消除文化和地域差异的角色,采用locale和facet可以为程序提供众多国际化支持,包括对各种字符集的支持,日期和时间的表示,数值和货币的处理等等。毕竟,在中国和在美国,人们表示日期的习惯是不同的。

容器(containers)部分,STL的一个重要组成部分,涵盖了许多数据结构,比如前面曾经提到的链表,还有:vector(类似于大小可动态增加的数组)、queue(队列)、stack(堆栈)……。string也可以看作是一个容器,适用于容器的方法同样也适用于string。现在你可以轻松的完成数据结构课程的家庭作业了。

算法(algorithms)部分,STL的一个重要组成部分,包含了大约70个通用算法,用于操控各种容器,同时也可以操控内建数组。比如:find用于在容器中查找等于某个特定值的元素,for_each用于将某个函数应用到容器中的各个元素上,sort用于对容器中的元素排序。所有这些操作都是在保证执行效率的前提下进行的,所以,如果在你使用了这些算法之后程序变得效率底下,首先一定不要怀疑这些算法本身,仔细检查一下程序的其他地方。

迭代器(iterators)部分,STL的一个重要组成部分,如果没有迭代器的撮合,容器和算法便无法结合的如此完美。事实上,每个容器都有自己的迭代器,只有容器自己才知道如何访问自己的元素。它有点像指针,算法通过迭代器来定位和操控容器中的元素。

数值(numerics)部分,包含了一些数学运算功能,提供了复数运算的支持。

输入/输出(input/output)部分,就是经过模板化了的原有标准库中的iostream部分,它提供了对C++程序输入输出的基本支持。在功能上保持了与原有iostream的兼容,并且增加了异常处理的机制,并支持国际化(internationalization)。

总体上,在C++标准函数库中,STL主要包含了容器、算法、迭代器。string也可以算做是STL的一部分。



图1:STL和C++标准函数库

正如前面所提到的,在STL的背后蕴含着泛型化程序设计(GP)的思想,在这种思想里,大部分基本算法被抽象,被泛化,独立于与之对应的数据结构,用于以相同或相近的方式处理各种不同情形。这一思想和面向对象的程序设计思想(OOP)不尽相同,因为,在OOP中更注重的是对数据的抽象,即所谓抽象数据类型(Abstract Data Type),而算法则通常被附属于数据类型之中。几乎所有的事情都可以被看作类或者对象(即类的实例),通常,我们所看到的算法被作为成员函数(member function)包含在类(class)中,类和类则构成了错综复杂的继承体系。

尽管在象C++这样的程序设计语言中,你还可以用全局函数来表示算法,但是在类似于Java这样的纯面向对象的语言中,全局函数已经被"勒令禁止"了。因此,用Java来模拟GP思想是颇为困难的。如果你对前述的STL历史还有印象的话,应该记得Alexander Stepanove也曾用基于OOP的语言尝试过实现GP思想,但是效果并不好,包括没有引入模板之前的C++语言。站在巨人的肩膀上,我们可以得出这样的结论,在OOP中所体现的思想与GP的思想确实是相异的。C++并不是一种纯面向对象的程序设计语言,它的绝妙之处,就在于既满足了OOP,又成全了GP。对于后者,模板立下了汗马功劳。另外,需要指出的是,尽管GP和OOP有诸多不同,但这种不同还不至于到"水火不容"的地步。并且,在实际运用的时候,两者的结合使用往往可以使问题的解决更为有效。作为GP思想实例的STL本身便是一个很好的范例,如果没有继承,不知道STL会是什么样子,似乎没有人做过这样的试验。

相信你对STL的感性认识应该有所提高了,是该做一些实际的工作了,那么我们首先来了解一下STL的不同实现版本。ANSI/ISO C++文件中的STL是一个仅被描述在纸上的标准,对于诸多C++编译器而言,需要有各自实际的STL,它们或多或少的实现了标准中所描述的内容,这样才能够为我们所用。之所以有不同的实现版本,则存在诸多原因,有历史的原因,也有各自编译器生产厂商的原因。以下是几个常见的STL实现版本。

HP STL是所有其它STL实现版本的根源。它是STL之父Alexander Stepanov在惠普的Palo Alto实验室工作时,和Meng Lee共同完成的,是第一个STL的实现版本(参见1.2节)。这个STL是开放源码的,所以它允许任何人免费使用、复制、修改、发布和销售该软件和相关文档,前提是必须在所有相关文件中加入HP STL的版本信息和授权信息。现在已经很少直接使用这个版本的STL了。

P. J. Plauger STL属于个人作品,由P. J. Plauger本人实现,是HP STL的一个继承版本,因此在其所有头文件中都含有HP STL的相关声明,同时还有P. J. Plauger本人的版权声明。P. J. Plauger是标准C中stdio库的早期实现者,现在是C/C++ User's Journal的主编,与Microsoft保持着良好的关系。P. J. Plauger STL便是被用于Microsoft的Visual C++中的。在Windows平台下的同类版本中,其性能不错,但是queue组件(队列,一种容器)的效率不理想,同时由于Visual C++对C++语言标准的支持不是很好(至少直到VC6.0为止,还是如此),因此一定程度上影响了P. J. Plauger STL的性能。此外,该版本的源代码可读性较差,你可以在VC的Include子目录下找到所有源文件(比如:C:\Program Files\Microsoft Visual Studio\VC98\Include)。因为不是开放源码的(open source),所以这些源代码是不能修改和销售的,目前P.J. Plauger STL由Dinkumware公司提供相关服务,详情请见

1.4.3 Rouge Wave STLhttp://www.rougewave.com。遗憾的是该版本已有一段时间没有更新且不完全符合标准。因此在Borland C++ Builder 6.0中,它的地位被另一个STL的实现版本--STLport(见后)取代了。但是考虑到与以前版本的兼容,C++ Builder 6.0还是保留了Rouge Wave STL,只是如果你想查看它的源代码的话,需要在别的目录中才能找到(比如:C:\Program Files\Borland\Cbuilder6\Include\oldstl)。

Rouge Wave STL是由Rouge Wave公司实现的,也是HP STL的一个继承版本,除了HP STL的相关声明之外,还有Rouge Wave公司的版权声明。同时,它也不是开放源码的,因此无法修改和销售。该版本被Borland C++ Builder所采用,你可以在C++ Builder的Include子目录下找到所有头文件(比如:C:\Program Files\Borland\Cbuilder5\Include)。尽管Rouge Wave STL的性能不是很好,但由于C++ Builder对C++语言标准的支持还算不错,使其表现在一定程度上得以改善。此外,其源代码的可读性较好。可以从如下网站得到更详细的情况介绍:

1.4.4 STLporthttp://www.stlport.org,可以免费下载其源代码。STLport已经被C/C++技术委员会接受成为工业标准,且在许多平台上都支持。根据测试STLport的效率比VC中的STL要快。比Rouge Wave STL更符合标准,也更容易移植。Borland C++ Builder已经在其6.0版中加入了对STLport的支持,它使用的STLport就是4.5版的,C++ Builder 6.0同时还提供了STLport的使用说明。你可以在C++ Builder的Include\Stlport子目录下找到所有头文件(比如:C:\Program Files\Borland\Cbuilder6\Include\Stlport)。

STLport最初源于俄国人Boris Fomitchev的一个开发项目,主要用于将SGI STL的基本代码移植到其他诸如C++Builder或者是Visual C++这样的主流编译器上。因为SGI STL属于开放源码,所以STLport才有权这样做。目前STLport的最新版本是4.5。可以从如下网站得到更详细的情况介绍:

1.4.5 SGI STLhttp://www.sgi.com,可以免费下载其源代码。目前的最新版本是3.3。

SGI STL是由Silicon Graphics Computer System, Inc公司实现的,其设计者和编写者包括Alexander Stepanov和Matt Austern,同样它也是HP STL的一个继承版本。它属于开放源码,因此你可以修改和销售它。SGI STL被GCC(linux下的C++编译器)所采用,你可以在GCC的Include子目录下找到所有头文件(比如:C:\cygnus\cygwin-b20\include\g++\include)。由于GCC对C++语言标准的支持很好,SGI STL在linux平台上的性能相当出色。此外,其源代码的可读性也很好。可以从如下网站得到更详细的情况介绍:



图2:STL家族的谱系

2 牛刀小试:且看一个简单例程2.1 引子2.2 例程实作2.2.1 第一版:史前时代--转木取火2.2.2 第二版:工业时代--组件化大生产2.2.3 第三版:唯美主义的杰作2.3 历史的评价2.4 如何运行

如果你是一个纯粹的实用主义者,也许一开始就可以从这里开始看起,因为此处提供了一个示例程序,它可以带给你有关使用STL的最直接的感受。是的,与其纸上谈兵,不如单刀直入,实际操作一番。但是,需要提醒的是,假如你在兴致昂然地细细品味本章内容的时候,能够同时结合前面章节作为佐餐,那将是再好不过的。你会发现,前面所提到的有关STL的那些优点,在此处得到了确切的应证。本章的后半部分,将为你演示在一些主流C++编译器上,运行上述示例程序的具体操作方法,和需要注意的事项。

非常遗憾,我不得不舍弃"Hello World"这个经典的范例,尽管它不只一次的被各种介绍计算机语言的教科书所引用,几乎成为了一个默认的“标准”。其原因在于它太过简单了,以至于不具备代表性,无法展现STL的巨大魅力。我选用了一个稍稍复杂一点的例子,它的大致功能是:从标准输入设备(一般是键盘)读入一些整型数据,然后对它们进行排序,最终将结果输出到标准输出设备(一般是显示器屏幕)。这是一种典型的处理方式,程序本身具备了一个系统所应该具有的几乎所有的基本特征:输入 + 处理 + 输出。你将会看到三个不同版本的程序。第一个是没有使用STL的普通C++程序,你将会看到完成这样看似简单的事情,需要花多大的力气,而且还未必没有一点问题(真是吃力不讨好)。第二个程序的主体部分使用了STL特性,此时在第一个程序中所遇到的问题就基本可以解决了。同时,你会发现采用了STL之后,程序变得简洁明快,清晰易读。第三个程序则将STL的功能发挥到了及至,你可以看到程序里几乎每一行代码都是和STL相关的。这样的机会并不总是随处可见的,它展现了STL中的几乎所有的基本组成部分,尽管这看起来似乎有点过分了。

有几点是需要说明的:

这个例程的目的,在于向你演示如何在C++程序中使用STL,同时希望通过实践,证明STL所带给你的确确实实的好处。程序中用到的一些STL基本组件,比如:vector(一种容器)、sort(一种排序算法),你只需要有一个大致的概念就可以了,这并不影响阅读代码和理解程序的含义。

很多人对GUI(图形用户界面)的运行方式很感兴趣,这也难怪,漂亮的界面总是会令人赏心悦目的。但是很可惜,在这里没有加入这些功能。这很容易解释,对于所提供的这个简单示例程序而言,加入GUI特性,是有点本末倒置的。这将会使程序的代码量骤然间急剧膨胀,而真正可以说明问题的核心部分确被淹没在诸多无关紧要的代码中间(你需要花去极大的精力来处理键盘或者鼠标的消息响应这些繁琐而又较为规范的事情)。即使你有像Borland C++ Builder这样的基于IDE(集成化开发环境)的工具,界面的处理变得较为简单了(框架代码是自动生成的)。请注意,我们这里所谈及的是属于C++标准的一部分(STL的第一个字母说明了这一点),它不涉及具体的某个开发工具,它是几乎在任何C++编译器上都能编译通过的代码。毕竟,在Microsoft Visual C++和Borland C++ Builder里,有关GUI的处理代码是不一样的。如果你想了解这些GUI的细节,这里恐怕没有你希望得到的答案,你可以寻找其它相关书籍。

在STL还没有降生的"黑暗时代",C++程序员要完成前面所提到的那些功能,需要做很多事情(不过这比起C程序来,似乎好一点),程序大致是如下这个样子的:

// name:example2_1.cpp
// alias:Rubish

#include <stdlib.h>
#include <iostream.h>

int compare(const void *arg1, const void *arg2);

void main(void)
{
	const int max_size = 10;		// 数组允许元素的最大个数
	int num[max_size];			// 整型数组

	// 从标准输入设备读入整数,同时累计输入个数,
	// 直到输入的是非整型数据为止
	int n;
        //zbf批注:使用cin输入数据时,中间用空格隔开!输入结束时先Enter然后Ctrl+z或者按Tab键,然后回车
	for (n = 0; cin >> num[n]; n ++);

	// C标准库中的快速排序(quick-sort)函数
	qsort(num, n, sizeof(int), compare);

	// 将排序结果输出到标准输出设备
	for (int i = 0; i < n; i ++)
		cout << num[i] << "\n";
}

// 比较两个数的大小,
// 如果*(int *)arg1比*(int *)arg2小,则返回-1
// 如果*(int *)arg1比*(int *)arg2大,则返回1
// 如果*(int *)arg1等于*(int *)arg2,则返回0
int compare(const void *arg1, const void *arg2)
{
	return	(*(int *)arg1 < *(int *)arg2) ? -1 :
			(*(int *)arg1 > *(int *)arg2) ? 1 : 0;
}
     

这是一个和STL没有丝毫关系的传统风格的C++程序。因为程序的注释已经很详尽了,所以不需要我再做更多的解释。总的说来,这个程序看起来并不十分复杂(本来就没有太多功能)。只是,那个compare函数,看起来有点费劲。指向它的函数指针被作为最后一个实参传入qsort函数,qsort是C程序库stdlib.h中的一个函数。以下是qsort的函数原型:

void qsort(void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );
	 

看起来有点令人作呕,尤其是最后一个参数。大概的意思是,第一个参数指明了要排序的数组(比如:程序中的num),第二个参数给出了数组的大小(qsort没有足够的智力预知你传给它的数组的实际大小),第三个参数给出了数组中每个元素以字节为单位的大小。最后那个长长的家伙,给出了排序时比较元素的方式(还是因为qsort的智商问题)。

以下是某次运行的结果:

输入:0 9 2 1 5
输出:0 1 2 5 9
	 

有一个问题,这个程序并不像看起来那么健壮(Robust)。如果我们输入的数字个数超过max_size所规定的上限,就会出现数组越界问题。如果你在Visual C++的IDE环境下以控制台方式运行这个程序时,会弹出非法内存访问的错误对话框。

这个问题很严重,严重到足以使你开始重新审视这个程序的代码。为了弥补程序中的这一缺陷。我们不得不考虑采用如下三种方案中的一种:

  • 采用大容量的静态数组分配。
  • 限定输入的数据个数。
  • 采用动态内存分配。

第一种方案比较简单,你所做的只是将max_size改大一点,比如:1000或者10000。但是,严格讲这并不能最终解决问题,隐患仍然存在。假如有人足够耐心,还是可以使你的这个经过纠正后的程序崩溃的。此外,分配一个大数组,通常是在浪费空间,因为大多数情况下,数组中的一部分空间并没有被利用。

再来看看第二种方案,通过在第一个for循环中加入一个限定条件,可以使问题得到解决。比如:for (int n = 0; cin >> num[n] && n < max_size; n ++); 但是这个方案同样不甚理想,尽管不会使程序崩溃,但失去了灵活性,你无法输入更多的数。

看来只有选择第三种方案了。是的,你可以利用指针,以及动态内存分配妥善的解决上述问题,并且使程序具有良好的灵活性。这需要用到new,delete操作符,或者古老的malloc(),realloc()和free()函数。但是为此,你将牺牲程序的简洁性,使程序代码陡增,代码的处理逻辑也不再像原先看起来那么清晰了。一个compare函数或许就已经令你不耐烦了,更何况要实现这些复杂的处理机制呢?很难保证你不会在处理这个问题的时候出错,很多程序的bug往往就是这样产生的。同时,你还应该感谢stdlib.h,它为你提供了qsort函数,否则,你还需要自己实现排序算法。如果你用的是冒泡法排序,那效率就不会很理想。……,问题真是越来越让人头疼了!

关于第一个程序的讨论就到此为止,如果你对第三种方案感兴趣的话,可以尝试着自己编写一个程序,作为思考题。这里就不准备再浪费笔墨去实现这样一个让人不甚愉快的程序了。

我们应该庆幸自己所生活的年代。工业时代,科技的发展所带来的巨大便利已经影响到了我们生活中的每个细节。如果你还在以原始人类的方式生活着,那我真该怀疑你是否属于某个生活在非洲或者南美丛林里的原始部落中的一员了,难道是玛雅文明又重现了?

STL便是这个时代的产物,正如其他科技成果一样,C++程序员也应该努力使自己适应并充分利用这个"高科技成果"。让我们重新审视第一版的那个破烂不堪的程序。试着使用一下STL,看看效果如何。

// name:example2_2.cpp
// alias:The first STL program

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void main(void)
{
	vector<int> num;		// STL中的vector容器
	int element;

	// 从标准输入设备读入整数, 
	// 直到输入的是非整型数据为止
	while (cin >> element)
		num.push_back(element);

	// STL中的排序算法
	sort(num.begin(), num.end());

	// 将排序结果输出到标准输出设备
	for (int i = 0; i < num.size(); i ++)
		cout << num[i] << "\n";
}
     

这个程序的主要部分改用了STL的部件,看起来要比第一个程序简洁一点,你已经找不到那个讨厌的compare函数了。它真的能很好的运行吗?你可以试试,因为程序的运行结果和前面的大致差不多,所以在此略去。我可以向你保证,这个程序是足够健壮的。不过,可能你还没有完全看明白程序的代码,所以我需要为你解释一下。毕竟,这个戏法变得太快了,较之第一个程序,一眨眼的功夫,那些老的C++程序员所熟悉的代码都不见了,取而代之的是一些新鲜玩意儿。

程序的前三行是包含的头文件,它们提供了程序所要用到的所有C++特性(包括输入输出处理,STL中的容器和算法)。不必在意那个.h,并不是我的疏忽,程序保证可以编译通过,只要你的C++编译器支持标准C++规范的相关部分。你只需要把它们看作是一些普通的C++头文件就可以了。事实上,也正是如此,如果你对这个变化细节感兴趣的化,可以留意一下你身旁的佐餐。

同样可以忽略第四行的存在。加入那个声明只是为了表明程序引用到了std这个标准名字空间(namespace),因为STL中的那些玩意儿全都包含在那里面。只有通过这行声明,编译器才能允许你使用那些有趣的特性。

程序中用到了vector,它是STL中的一个标准容器,可以用来存放一些元素。你可以把vector理解为int [?],一个整型的数组。之所以大小未知是因为,vector是一个可以动态调整大小的容器,当容器已满时,如果再放入元素则vector会悄悄扩大自己的容量。push_back是vector容器的一个类属成员函数,用来在容器尾端插入一个元素。main函数中第一个while循环做的事情就是不断向vector容器尾端插入整型数据,同时自动维护容器空间的大小。

sort是STL中的标准算法,用来对容器中的元素进行排序。它需要两个参数用来决定容器中哪个范围内的元素可以用来排序。这里用到了vector的另两个类属成员函数。begin()用以指向vector的首端,而end()则指向vector的末端。这里有两个问题,begin()和end()的返回值是什么?这涉及到STL的另一个重要部件--迭代器(Iterator),不过这里并不需要对它做详细了解。你只需要把它当作是一个指针就可以了,一个指向整型数据的指针。相应的sort函数声明也可以看作是void sort(int* first, int* last),尽管这实际上很不精确。另一个问题是和end()函数有关,尽管前面说它的返回值指向vector的末端,但这种说法不能算正确。事实上,它的返回值所指向的是vector中最末端元素的后面一个位置,即所谓pass-the-end value。这听起来有点费解,不过不必在意,这里只是稍带一提。总的来说,sort函数所做的事情是对那个准整型数组中的元素进行排序,一如第一个程序中的那个qsort,不过比起qsort来,sort似乎要简单了许多。

程序的最后是输出部分,在这里vector完全可以以假乱真了,它所提供的对元素的访问方式简直和普通的C++内建数组一模一样。那个size函数用来返回vector中的元素个数,就相当于第一个程序中的变量n。这两行代码直观的不用我再多解释了。

我想我的耐心讲解应该可以使你大致看懂上面的程序了,事实上STL的运用使程序的逻辑更加清晰,使代码更易于阅读。试问,有谁会不明白begin、end、size这样的字眼所表达的含义呢(除非他不懂英语)?试着运行一下,看看效果。再试着多输入几个数,看看是否会发生数组越界现象。实践证明,程序运行良好。是的,由于vector容器自行维护了自身的大小,C++程序员就不用操心动态内存分配了,指针的错误使用毕竟会带来很多麻烦,同时程序也会变得冗长无比。这正是前面第三种方案的缺点所在。

再仔细审视一下你的第一个STL版的C++程序,回顾一下第一章所提到的那些有关STL的优点:易于使用,具有工业强度……,再比较一下第一版的程序,我想你应该有所体会了吧!

事态的发展有时候总会趋向极端,这在那些唯美主义者当中犹是如此。首先声明,我并不是一个唯美主义者,提供第二版程序的改进版,完全是为了让你更深刻的感受到STL的魅力所在。在看完第三版之后,你会强烈感受到这一点。或许你也会变成一个唯美主义者了,至少在STL方面。这应该不是我的错,因为决定权在你手里。下面我们来看看这个绝版的C++程序。

// name:example2_3.cpp
// alias:aesthetic version

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>

using namespace std;

void main(void)
{
	typedef vector<int>				int_vector;
	typedef istream_iterator<int>				istream_itr;
	typedef ostream_iterator<int>				ostream_itr;
	typedef back_insert_iterator< int_vector >	back_ins_itr;

	// STL中的vector容器
	int_vector num;

	// 从标准输入设备读入整数, 
	// 直到输入的是非整型数据为止
	copy(istream_itr(cin), istream_itr(), back_ins_itr(num));

	// STL中的排序算法
	sort(num.begin(), num.end());

	// 将排序结果输出到标准输出设备
	copy(num.begin(), num.end(), ostream_itr(cout, "\n"));
}
     

在这个程序里几乎每行代码都是和STL有关的(除了main和那对花括号,当然还有注释),并且它包含了STL中几乎所有的各大部件(容器container,迭代器iterator, 算法algorithm, 适配器adaptor),唯一的遗憾是少了函数对象(functor)的身影。

还记得开头提到的一个典型系统所具有的基本特征吗?--输入+处理+输出。所有这些功能,在上面的程序里,仅仅是通过三行语句来实现的,其中每一行语句对应一种操作。对于数据的操作被高度的抽象化了,而算法和容器之间的组合,就像搭积木一样轻松自如,系统的耦合度被降到了极低点。这就是闪耀着泛型之光的STL的伟大力量。如此简洁,如此巧妙,如此神奇!就像魔术一般,以至于再一次让你摸不着头脑。怎么实现的?为什么在看第二版程序的时候如此清晰的你,又坠入了五里雾中(窃喜)。

请留意此处的标题(唯美主义的杰作),在实际环境中,你未必要做到这样完美。毕竟美好愿望的破灭,在生活中时常会发生。过于理想化,并不是一件好事,至少我是这么认为的。正如前面提到的,这个程序只是为了展示STL的独特魅力,你不得不为它的出色表现所折服,也许只有深谙STL之道的人才会想出这样的玩意儿来。如果你只是一般性的使用STL,做到第二版这样的程度也就可以了。

实在是因为这个程序太过"简单",以至于我无法肯定,在你还没有完全掌握STL之前,通过我的讲解,是否能够领会这区区三行代码,我将尽我的最大努力。

前面提到的迭代器可以对容器内的任意元素进行定位和访问。在STL里,这种特性被加以推广了。一个cin代表了来自输入设备的一段数据流,从概念上讲它对数据流的访问功能类似于一般意义上的迭代器,但是C++中的cin在很多地方操作起来并不像是一个迭代器,原因就在于其接口和迭代器的接口不一致(比如:不能对cin进行++运算,也不能对之进行取值运算--即*运算)。为了解决这个矛盾,就需要引入适配器的概念。istream_iterator便是一个适配器,它将cin进行包装,使之看起来像是一个普通的迭代器,这样我们就可以将之作为实参传给一些算法了(比如这里的copy算法)。因为算法只认得迭代器,而不会接受cin。对于上面程序中的第一个copy函数而言,其第一个参数展开后的形式是:istream_iterator(cin),其第二个参数展开后的形式是:istream_iterator()(如果你对typedef的语法不清楚,可以参考有关的c++语言书籍)。其效果是产生两个迭代器的临时对象,前一个指向整型输入数据流的开始,后一个则指向"pass-the-end value"。这个函数的作用就是将整型输入数据流从头至尾逐一"拷贝"到vector这个准整型数组里,第一个迭代器从开始位置每次累进,最后到达第二个迭代器所指向的位置。或许你要问,如果那个copy函数的行为真如我所说的那样,为什么不写成如下这个样子呢?

copy(istream_iterator<int>(cin), istream_iterator<int>(), num.begin());
	 

你确实可以这么做,但是有一个小小的麻烦。还记得第一版程序里的那个数组越界问题吗?如果你这么写的话,就会遇到类似的麻烦。原因在于copy函数在"拷贝"数据的时候,如果输入的数据个数超过了vector容器的范围时,数据将会拷贝到容器的外面。此时,容器不会自动增长容量,因为这只是简单地拷贝,并不是从末端插入。为了解决这个问题,另一个适配器back_insert_iterator登场了,它的作用就是引导copy算法每次在容器末端插入一个数据。程序中的那个back_ins_itr(num)展开后就是:back_insert_iterator(num),其效果是生成一个这样的迭待器对象。

终于将讲完了三分之一(真不容易!),好在第二句和前一版程序没有差别,这里就略过了。至于第三句,ostream_itr(cout, "\n")展开后的形式是:ostream_iterator(cout, "\n"),其效果是产生一个处理输出数据流的迭待器对象,其位置指向数据流的起始处,并且以"\n"作为分割符。第二个copy函数将会从头至尾将vector中的内容"拷贝"到输出设备,第一个参数所代表的迭代器将会从开始位置每次累进,最后到达第二个参数所代表的迭代器所指向的位置。

这就是全部的内容。

历史的车轮总是滚滚向前的,工业时代的文明较之史前时代,当然是先进并且发达的。回顾那两个时代的C++程序,你会真切的感受到这种差别。简洁易用,具有工业强度,较好的可移植性,高效率,加之第三个令人目眩的绝版程序所体现出来的高度抽象性,高度灵活性和组件化特性,使你对STL背后所蕴含的泛型化思想都有了些微的感受。

真幸运,你可以横跨两个时代,有机会目睹这种"文明"的差异。同时,这也应该使你越加坚定信念,使自己顺应时代的潮流。

在你还没有真正开始运行前面后两个程序之前,最好先浏览一下本节。这里简单介绍了在特定编译器环境下运行STL程序的一些细节,并提供了一些可能遇到的问题的解决办法。

此处,我选用了目前在Windows平台下较为常见的Microsoft Visual C++ 6.0和Borland C++ Builder 6.0作为例子。尽管Visual C++ 6.0对最新的ANSI/ISO C++标准支持的并不是很好。不过据称Visual C++ .NET(也就是VC7.0)在这方面的性能有所改善。

你可以选用多种方式运行前面的程序,比如在Visual C++下,你可以直接在DOS命令行状态下编译运行,也可以在VC的IDE下采用控制台应用程序(Console Application)的方式运行。对于C++ Builder,情况也类似。

对于Visual C++而言,如果是在DOS命令行状态下,你首先需要找到它的编译器。假定你的Visual C++装在C:\Program Files\Microsoft Visual Studio\VC98下面,则其编译器所在路径应该是C:\Program Files\Microsoft Visual Studio\VC98\Bin,在那里你可以找到cl.exe文件。编译时请加上/GX和/MT参数。如果一切正常,结果就会产生一个可执行文件。如下所示:

cl /GX /MT example2_2.cpp
     

前一个参数用于告知编译器允许异常处理(Exception Handling)。在P. J. Plauger STL中的很多地方使用了异常处理机制(即try…throw…catch语法),所以应该加上这个参数,否则会有如下警告信息:

warning C4530: C++ exception handler used, but unwind semantics are not enabled.
     

后一个参数则用于使程序支持多线程,它需要在链接时使用LIBCMT.LIB库文件。不过P. J. Plauger STL并不是线程安全的(thread safety)。如果你是在VC环境下使用像STLport这样的STL实现版本,则需要加上这个参数,因为STLport是线程安全的。

如果在IDE环境下,可以在新建工程的时候选择控制台应用程序。



图3:在Visual C++ IDE环境下运行STL程序

至于那些参数的设置,则可以通过在Project功能菜单项中的Settings功能【Alt+F7】中设置编译选项来完成。



图4:在Visual C++ IDE环境下设置编译参数

有时,在IDE环境下编译STL程序时,可能会出现如下警告信息(前面那几个示例程序不会出现这种情况):

warning C4786: '……' : identifier was truncated to '255' characters in the debug information 
     

这是因为编译器在Debug状态下编译时,把程序中所出现的标识符长度限制在了255个字符范围内。如果超过最大长度,这些标识符就无法在调试阶段查看和计算了。而在STL程序中大量的用到了模板函数和模板类,编译器在实例化这些内容时,展开之后所产生的标识符往往很长(没准会有一千多个字符!)。如果你想认识一下这个warning的话,很简单,在程序里加上如下一行代码:

vector<string>		string_array;		// 类似于字符串数组变量
     

对于这样的warning,当然可以置之不理,不过也是有解决办法的。 你可以在文件开头加入下面这一行:#pragma warning(disable: 4786)。它强制编译器忽略这个警告信息,这种做法虽然有点粗鲁,但是很有效。

注意:zbf):使用右键点击项目工程中的该cpp文件,选择setting,在c/c++栏,选择PreCompiled headers,然后设置第一选项,选择不使用预编译头!否则会出现如下问题:

fatal error C1010: unexpected end of file while looking for precompiled header directive

至于C++ Builder,其DOS命令行状态下的运行方式是这样的。假如你的C++ Builder装在C:\Program Files\Borland\CBuilder6。则其编译器所在路径应该是C:\Program Files\ Borland\CBuilder6\Bin,在那里你可以找到bcc32.exe文件,输入如下命令,即大功告成了:

bcc32 example2_2.cpp
	 

至于IDE环境下,则可以在新建应用程序的时候,选择控制台向导(Console Wizard)。



图5:在C++ Builder IDE环境下运行STL程序

现在你可以在你的机器上运行前面的示例程序了。不过,请恕我多嘴,有些细节不得不提请你注意。小心编译器给你留下的陷阱。比如前面第三个程序中有如下这一行代码:

typedef back_insert_iterator< int_vector >	back_ins_itr;
     

请留意">"前面的空格,最好不要省去。如果你吝惜这点空格所占用的磁盘空间的话,那就太不划算了。其原因还是在于C++编译器本身的缺陷。上述代码,相当于如下代码(编译器做的也正是这样的翻译工作):

typedef back_insert_iterator< vector<int> >	back_ins_itr;
     

如果你没有加空格的话,编译器会把">>"误认为是单一标识(看起来很像那个数据流输入操作符">>")。为了回避这个难题,C++要求使用者必须在两个右尖括号之间插入空格。所以,你最好还是老老实实照我的话做,以避免不必要的麻烦。不过有趣的是,对于上述那行展开前的代码,在Visual C++里即使你没有加空格,编译器也不会报错。而同样的代码在C++ Builder中没有那么幸运了。不过,最好还是不要心存侥幸,如果你采用展开后的书写方式,则两个编译器都不会给你留情面了。

好了,请原谅我的絮叨,现在你可以亲身感受一下STL所带给你的真正独特魅力了,祝你好运!




标准模板库(STL)介绍

作者: Scott Field
来源:最优秀的STL学习网站

本文以List容器为例子,介绍了STL的基本内容,从容器到迭代器,再到普通函数,而且例子丰富,通俗易懂。不失为STL的入门文章,新手不容错过!

0 前言.


这篇文章是关于C++语言的一个新的扩展——标准模板库的(Standard Template Library),也叫STL。

当我第一次打算写一篇关于STL的文章的时候,我不得不承认我当时低估了这个话题的深度和广度。有很多内容要含盖,也有很多详细描述STL的书。因此我重新考虑了一下我原来的想法。我为什么要写这篇文章,又为什么要投稿呢?这会有什麽用呢?有再来一篇关于STL的文章的必要吗?

当我翻开Musser and Saini的页时,我看到了编程时代在我面前消融。我能看到深夜消失了, 目标软件工程出现了。我看到了可维护的代码。一年过去了,我使用STL写的软件仍然很容易维护。 让人吃惊的是其他人可以没有我而维护的很好!

然而,我也记得在一开始的时候很难弄懂那些技术术语。一次,我买了Musser&Saini,每件事都依次出现,但是在那以前我最渴望得到的东西是一些好的例子。

当我开始的时候,作为C++一部分的Stroustrup还没出来,它覆盖了STL。

因此我想写一篇关于一个STL程序员的真实生活的文章可能会有用。如果我手上有一些好的例子的话,特别是象这样的新题目,我会学的更快。

另外一件事是STL应该很好用。因此,理论上说,我们应该可以马上开始使用STL。

什麽是STL呢?STL就是Standard Template Library,标准模板库。这可能是一个历史上最令人兴奋的工具的最无聊的术语。从根本上说,STL是一些“容器”的集合,这些“容器”有list, vector,set,map等,STL也是算法和其他一些组件的集合。这里的“容器”和算法的集合指的是世界上很多聪明人很多年的杰作。

STL的目的是标准化组件,这样你就不用重新开发它们了。你可以仅仅使用这些现成的组件。STL现在是C++的一部分,因此不用额外安装什麽。它被内建在你的编译器之内。因为STL的list是一个简单的容器,所以我打算从它开始介绍STL如何使用。如果你懂得了这个概念,其他的就都没有问题了。另外, list容器是相当简单的,我们会看到这一点。

这篇文章中我们将会看到如何定义和初始化一个list,计算它的元素的数量,从一个list里查找元素,删除元素,和一些其他的操作。要作到这些,我们将会讨论两个不同的算法,STL通用算法都是可以操作不止一个容器的,而list的成员函数是list容器专有的操作。

这是三类主要的STL组件的简明纲要。STL容器可以保存对象,内建对象和类对象。它们会安全的保存对象,并定义我们能够操作的这个对象的接口。放在蛋架上的鸡蛋不会滚到桌上。它们很安全。因此,在STL容器中的对象也很安全。我知道这个比喻听起来很老土,但是它很正确。

STL算法是标准算法,我们可以把它们应用在那些容器中的对象上。这些算法都有很著名的执行特性。它们可以给对象排序,删除它们,给它们记数,比较,找出特殊的对象,把它们合并到另一个容器中,以及执行其他有用的操作。

STL iterator就象是容器中指向对象的指针。STL的算法使用iterator在容器上进行操作。Iterator设置算法的边界 ,容器的长度,和其他一些事情。举个例子,有些iterator仅让算法读元素,有一些让算法写元素,有一些则两者都行。 Iterator也决定在容器中处理的方向。

你可以通过调用容器的成员函数begin()来得到一个指向一个容器起始位置的iterator。你可以调用一个容器的 end() 函数来得到过去的最后一个值(就是处理停在那的那个值)。

这就是STL所有的东西,容器、算法、和允许算法工作在容器中的元素上的iterator。 算法以合适、标准的方法操作对象,并可通过iterator得到容器精确的长度。一旦做了这些,它们就在也不会“跑出边界”。 还有一些其他的对这些核心组件类型有功能性增强的组件,例如函数对象。我们将会看到有关这些的例子,现在 ,我们先来看一看STL的list。

1 定义一个list


我们可以象这样来定义一个STL的list:
#include <string>
#include <list>
using namespace std;
int main (void) 
{
  list<string> Milkshakes;
  return 0;
}

这就行了,你已经定义了一个list。简单吗?list<string> Milkshakes这句是你声明了list<string>模板类 的一个实例,然后就是实例化这个类的一个对象。但是我们别急着做这个。在这一步其实你只需要知道你定义了 一个字符串的list。你需要包含提供STL list类的头文件。我用gcc 2.7.2在我的Linux上编译这个测试程序,例如:

g++ test1.cpp -o test1

注意iostream.h这个头文件已经被STL的头文件放弃了。这就是为什么这个例子中没有它的原因。

现在我们有了一个list,我们可以看实使用它来装东西了。我们将把一个字符串加到这个list里。有一个非常 重要的东西叫做list的值类型。值类型就是list中的对象的类型。在这个例子中,这个list的值类型就是字符串,string , 这是因为这个list用来放字符串。

2 使用list的成员函数push_back和push_front插入一个元素到list中


#include <string>
#include <list>
using namespace std;
int main (void) 
{
  list<string> Milkshakes;
  Milkshakes.push_back("Chocolate");
  Milkshakes.push_back("Strawberry");
  Milkshakes.push_front("Lime");
  Milkshakes.push_front("Vanilla");
  return 0;
}

我们现在有个4个字符串在list中。list的成员函数push_back()把一个对象放到一个list的后面,而 push_front()把对象放到前面。我通常把一些错误信息push_back()到一个list中去,然后push_front()一个标题到list中, 这样它就会在这个错误消息以前打印它了。

3 list的成员函数empty()


知道一个list是否为空很重要。如果list为空,empty()这个成员函数返回真。 我通常会这样使用它。通篇程序我都用push_back()来把错误消息放到list中去。然后,通过调用empty() 我就可以说出这个程序是否报告了错误。如果我定义了一个list来放信息,一个放警告,一个放严重错误, 我就可以通过使用empty()轻易的说出到底有那种类型的错误发生了。

我可以整理这些list,然后在打印它们之前,用标题来整理它们,或者把它们排序成类。

这是我的意思:

/*
|| Using a list to track and report program messages and status 
*/
//不要使用#include <iostream.h>
#include <iostream>
#include <string>
#include <list>
using namespace std;
int main (void) 
{
  #define OK 0 
  #define INFO 1
  #define WARNING 2
  int return_code;
  list<string> InfoMessages;
  list<string> WarningMessages;

  // during a program these messages are loaded at various points
  InfoMessages.push_back("Info: Program started");
  // do work...
  WarningMessages.push_back("Warning: No Customer records have been found");
  // do work...
 
  return_code = OK; 
 
  if  (!InfoMessages.empty()) {          // there were info messages
     InfoMessages.push_front("Informational Messages:");
     // ... print the info messages list, we'll see how later
     return_code = INFO;
  }

  if  (!WarningMessages.empty()) {       // there were warning messages
     WarningMessages.push_front("Warning Messages:");
     // ... print the warning messages list, we'll see how later
     return_code = WARNING;              
  }

  // If there were no messages say so.
  if (InfoMessages.empty() && WarningMessages.empty()) {
     cout << "There were no messages " << endl;
  }

  return return_code;
}

4 用for循环来处理list中的元素

我们想要遍历一个list,比如打印一个中的所有对象来看看list上不同操作的结果。要一个元素一个元素的遍历一个list, 我们可以这样做:

/*
|| How to print the contents of a simple STL list. Whew! 
*/
//不要使用#include <iostream.h>
#include <iostream>
#include <string>
#include <list>
using namespace std;
void main (void) 
{
  list<string> Milkshakes;
  list<string>::iterator MilkshakeIterator;

  Milkshakes.push_back("Chocolate");
  Milkshakes.push_back("Strawberry");
  Milkshakes.push_front("Lime");
  Milkshakes.push_front("Vanilla");
  
  // print the milkshakes
  Milkshakes.push_front("The Milkshake Menu");
  Milkshakes.push_back("*** Thats the end ***");
  for (MilkshakeIterator=Milkshakes.begin(); 
         MilkshakeIterator!=Milkshakes.end(); 
          ++MilkshakeIterator) 
  {
    // dereference the iterator to get the element
    cout << *MilkshakeIterator << endl;
  }     
}

这个程序定义了一个iterator,MilkshakeIterator。我们把它指向了这个list的第一个元素。 这可以调用Milkshakes.begin()来作到,它会返回一个指向list开头的iterator。然后我们把它和Milkshakes.end()的 返回值来做比较,当我们到了那儿的时候就停下来。

容器的end()函数会返回一个指向容器的最后一个位置的iterator。当我们到了那里,就停止操作。 我们不能不理容器的end()函数的返回值。我们仅知道它意味着已经处理到了这个容器的末尾,应该停止处理了。 所有的STL容器都要这样做。

在上面的例子中,每一次执行for循环,我们就重复引用iterator来得到我们打印的字符串。

在STL编程中,我们在每个算法中都使用一个或多个iterator。我们使用它们来存取容器中的对象。 要存取一个给定的对象,我们把一个iterator指向它,然后间接引用这个iterator。

这个list容器,就象你所想的,它不支持在iterator加一个数来指向隔一个的对象。 就是说,我们不能用Milkshakes.begin()+2来指向list中的第三个对象,因为STL的list是以双链的list来实现的, 它不支持随机存取。vector和deque(向量和双端队列)和一些其他的STL的容器可以支持随机存取。

上面的程序打印出了list中的内容。任何人读了它都能马上明白它是怎麽工作的。它使用标准的iterator和标准 的list容器。没有多少程序员依赖它里面装的东西, 仅仅是标准的C++。这是一个向前的重要步骤。这个例子使用STL使我们的软件更加标准。

5 用STL的通用算法for_each来处理list中的元素


使用STL list和 iterator,我们要初始化、比较和给iterator增量来遍历这个容器。STL通用的for_each 算法能够减轻我们的工作。
 /*
|| How to print a simple STL list MkII
*/
//不要使用#include <iostream.h>
#include <iostream>
#include <string>
#include <list>
#include <algorithm>
using namespace std;
PrintIt (string& StringToPrint) {
  cout << StringToPrint << endl;
}

void main (void) {
  list<string> FruitAndVegetables;
  FruitAndVegetables.push_back("carrot");
  FruitAndVegetables.push_back("pumpkin");
  FruitAndVegetables.push_back("potato");
  FruitAndVegetables.push_front("apple");
  FruitAndVegetables.push_front("pineapple");
 
  for_each  (FruitAndVegetables.begin(), FruitAndVegetables.end(), PrintIt);
}
在这个程序中我们使用STL的通用算法for_each()来遍历一个iterator的范围,然后调用PrintIt()来处理每个对象。 我们不需要初始化、比较和给iterator增量。for_each()为我们漂亮的完成了这些工作。我们执行于对象上的 操作被很好的打包在这个函数以外了,我们不用再做那样的循环了,我们的代码更加清晰了。

for_each算法引用了iterator范围的概念,这是一个由起始iterator和一个末尾iterator指出的范围。 起始iterator指出操作由哪里开始,末尾iterator指明到哪结束,但是它不包括在这个范围内。用STL的通用算法count()来统计list中的元素个数。

STL的通用算法count()和count_it()用来给容器中的对象记数。就象for_each()一样,count()和count_if() 算法也是在iterator范围内来做的。

让我们在一个学生测验成绩的list中来数一数满分的个数。这是一个整型的List。

 /*
|| How to count objects in an STL list
*/
#include <list>
#include <algorithm>
#include <iostream>
using namespace std;
void main (void) 
{
  list<int> Scores;
  Scores.push_back(100); Scores.push_back(80);
  Scores.push_back(45); Scores.push_back(75);
  Scores.push_back(99); Scores.push_back(100);
  int NumberOf100Scores(0); 
    
  //此行有错count (Scores.begin(), Scores.end(), 100, NumberOf100Scores); 
  NumberOf100Scores=count (Scores.begin(), Scores.end(), 100)
  cout << "There were " << NumberOf100Scores << " scores of 100" << endl;
}
count()算法统计等于某个值的对象的个数。上面的例子它检查list中的每个整型对象是不是100。每次容器中的对象等于100,它就给NumberOf100Scores加1。这是程序的输出:

There were 2 scores of 100

zbf批注:count的算法如下

template<class _II, class _Ty> inline
 _CNTSIZ(_II) count(_II _F, _II _L, const _Ty& _V)
 {_CNTSIZ(_II) _N = 0;
 for (; _F != _L; ++_F)
  if (*_F == _V)
   ++_N;
 return (_N); }

6 用STL的通用算法count_if()来统计list中的元素个数


count_if()是count()的一个更有趣的版本。他采用了STL的一个新组件,函数对象。count_if() 带一个函数对象的参数。函数对象是一个至少带有一个operator()方法的类。有些STL算法作为参数接收 函数对象并调用这个函数对象的operator()方法。

函数对象被约定为STL算法调用operator时返回true或false。它们根据这个来判定这个函数。举个例子会 说的更清楚些。count_if()通过传递一个函数对象来作出比count()更加复杂的评估以确定一个对象是否应该被 记数。在这个例子里我们将数一数牙刷的销售数量。我们将提交包含四个字符的销售码和产品说明的销售记录。

 
/*
|| Using a function object to help count things
*/
#include <string>
#include <list>
#include <algorithm>
#include <iostream>
using namespace std;

const string ToothbrushCode("0003");

class IsAToothbrush 
{
public:  
   bool operator() ( string& SalesRecord ) 
   {
     return SalesRecord.substr(0,4)==ToothbrushCode;
   }     
};

void main (void) 
{
  list<string> SalesRecords;

  SalesRecords.push_back("0001 Soap");
  SalesRecords.push_back("0002 Shampoo");
  SalesRecords.push_back("0003 Toothbrush");
  SalesRecords.push_back("0004 Toothpaste");
  SalesRecords.push_back("0003 Toothbrush");
  
  int NumberOfToothbrushes(0);  
  NumberOfToothbrushes=count_if (SalesRecords.begin(), SalesRecords.end(), 
             IsAToothbrush());

  cout << "There were " 
       << NumberOfToothbrushes 
       << " toothbrushes sold" << endl;
}
这是这个程序的输出:

There were 2 toothbrushes sold

这个程序是这样工作的:定义一个函数对象类IsAToothbrush,这个类的对象能判断出卖出的是否是牙刷 。如果这个记录是卖出牙刷的记录的话,函数调用operator()返回一个true,否则返回false。

count_if()算法由第一和第二两个iterator参数指出的范围来处理容器对象。它将对每个 IsAToothbrush?()返回true的容器中的对象增加NumberOfToothbrushes的值。

最后的结果是NumberOfToothbrushes这个变量保存了产品代码域为"0003"的记录的个数,也就是牙刷的个数。

注意count_if()的第三个参数IsAToothbrush(),它是由它的构造函数临时构造的一个对象。你可以把IsAToothbrush类的一个临时对象 传递给count_if()函数。count_if()将对该容器的每个对象调用这个函数。

 

7 使用count_if()的一个更加复杂的函数对象。


我们可以更进一步的研究一下函数对象。假设我们需要传递更多的信息给一个函数对象。我们不能通过 调用operator来作到这点,因为必须定义为一个list的中的对象的类型。 然而我们通过为IsAToothbrush指出一个非缺省的构造函数就可以用任何我们所需要的信息来初始化它了。 例如,我们可能需要每个牙刷有一个不定的代码。我们可以把这个信息加到下面的函数对象中:

 
/*
|| Using a more complex function object
*/
#include <iostream>
#include <string>
#include <list>
#include <algorithm>
using namespace std;
class IsAToothbrush 
{
public:
  IsAToothbrush(string& InToothbrushCode) : 
      ToothbrushCode(InToothbrushCode) {}
  bool operator() (string& SalesRecord) 
  {
    return SalesRecord.substr(0,4)==ToothbrushCode;
  }       
private:
  string ToothbrushCode;        
};

void main (void) 
{
  list<string> SalesRecords;

  SalesRecords.push_back("0001 Soap");
  SalesRecords.push_back("0002 Shampoo");
  SalesRecords.push_back("0003 Toothbrush");
  SalesRecords.push_back("0004 Toothpaste");
  SalesRecords.push_back("0003 Toothbrush");
  
  string VariableToothbrushCode("0003");

  int NumberOfToothbrushes(0);  
  NumberOfToothbrushes=count_if (SalesRecords.begin(), SalesRecords.end(), 
              IsAToothbrush(VariableToothbrushCode));
  cout << "There were  "
       << NumberOfToothbrushes 
       << " toothbrushes matching code "
       << VariableToothbrushCode
       << " sold" 
       << endl;
}
程序的输出是:

There were 2 toothbrushes matching code 0003 sold

这个例子演示了如何向函数对象传递信息。你可以定义任意你想要的构造函数,你可以在函数对象中做任何你 想做的处理,都可以合法编译通过。

你可以看到函数对象真的扩展了基本记数算法。

到现在为止,我们都学习了:

  • 定义一个list
  • 向list中加入元素
  • 如何知道list是否为空
  • 如何使用for循环来遍历一个list
  • 如何使用STL的通用算法for_each来遍历list
  • list成员函数begin() 和 end() 以及它们的意义
  • iterator范围的概念和一个范围的最后一个位置实际上并不被处理这一事实
  • 如何使用STL通用算法count()和count_if()来对一个list中的对象记数
  • 如何定义一个函数对象

我选用这些例子来演示list的一般操作。如果你懂了这些基本原理,你就可以毫无疑问的使用STL了 建议你作一些练习。我们现在用一些更加复杂的操作来扩展我们的知识,包括list成员函数和STL通用算法。

8 使用STL通用算法find()在list中查找对象


  我们如何在list中查找东西呢?STL的通用算法find()和find_if()可以做这些。 就象for_each(), count(), count_if() 一样,这些算法也使用iterator范围,这个范围指出一个list或任意 其他容器中的一部分来处理。通常首iterator指着开始的位置,次iterator指着停止处理的地方。 由次iterator指出的元素不被处理。这是find()如何工作:
/*
|| How to find things in an STL list
*/
#include <string>
#include <list>
#include <algorithm>
#include <iostream>
using namespace std; void main (void) { list<string> Fruit; list<string>::iterator FruitIterator; Fruit.push_back("Apple"); Fruit.push_back("Pineapple"); Fruit.push_back("Star Apple"); FruitIterator = find (Fruit.begin(), Fruit.end(), "Pineapple"); if (FruitIterator == Fruit.end()) { cout << "Fruit not found in list" << endl; } else { cout << *FruitIterator << endl; } }
输出是:

Pineapple

如果没有找到指出的对象,就会返回Fruit.end()的值,要是找到了就返回一个指着找到的对象的iterator

9 使用STL通用算法find_if()在list中搜索对象


这是find()的一个更强大的版本。这个例子演示了find_if(),它接收一个函数对象的参数作为参数, 并使用它来做更复杂的评价对象是否和给出的查找条件相付。假设我们的list中有一些按年代排列的包含了事件和日期的记录。我们希望找出发生在1997年的事件。
/*
|| How to find things in an STL list MkII
*/
#include <string>
#include <list>
#include <algorithm>
 
class EventIsIn1997 {
public:
    bool operator () (string& EventRecord) {
        // year field is at position 12 for 4 characters in EventRecord
        return EventRecord.substr(12,4)=="1997";
    }
};
 
void main (void) {
    list<string> Events;
 
    // string positions 0123456789012345678901234567890123456789012345
    Events.push_back("07 January  1995 Draft plan of house prepared");
    Events.push_back("07 February 1996 Detailed plan of house prepared");
    Events.push_back("10 January  1997 Client agrees to job");
    Events.push_back("15 January  1997 Builder starts work on bedroom");
    Events.push_back("30 April    1997 Builder finishes work");
 
    list<string>::iterator EventIterator = find_if (Events.begin(), Events.end(), EventIsIn1997());
 
    // find_if completes the first time EventIsIn1997()() returns true
    // for any object. It returns an iterator to that object which we
    // can dereference to get the object, or if EventIsIn1997()() never
    // returned true, find_if returns end()
    if (EventIterator==Events.end()) {
        cout << "Event not found in list" << endl;
    }
    else {
        cout << *EventIterator << endl;
    }
}
这是程序的输出:

10 January 1997 Client agrees to job

10 使用STL通用算法search在list中找一个序列


一些字符在STL容器中很好处理,让我们看一看一个难处理的字符序列。我们将定义一个list来放字符。

list<char> Characters;

现在我们有了一个字符序列,它不用任何帮助就知道然后管理内存。它知道它是从哪里开始、到哪里结束。 它非常有用。我不知道我是否说过以null结尾的字符数组。

让我们加入一些我们喜欢的字符到这个list中:

Characters.push_back('\0');
Characters.push_back('\0');
Characters.push_back('1');
Characters.push_back('2');
我们将得到多少个空字符呢?
int NumberOfNullCharacters(0);
count(Characters.begin(), Characters.end(), '\0', NumberOfNullCharacters);
cout << "We have " << NumberOfNullCharacters << endl;
让我们找字符'1'
list<char>::iterator Iter;
Iter = find(Characters.begin(), Characters.end(), '1');
cout << "We found " << *Iter << endl;
这个例子演示了STL容器允许你以更标准的方法来处理空字符。现在让我们用STL的search算法来搜索容器中 的两个null。就象你猜的一样,STL通用算法search()用来搜索一个容器,但是是搜索一个元素串,不象find()和find_if() 只搜索单个的元素。
/*
|| How to use the search algorithm in an STL list
*/
#include <string>
#include <list>
#include <algorithm>
#include <iostream>
using namespace std; void main ( void){ list<char> TargetCharacters; list<char> ListOfCharacters; TargetCharacters.push_back('\0'); TargetCharacters.push_back('\0'); ListOfCharacters.push_back('1'); ListOfCharacters.push_back('2'); ListOfCharacters.push_back('\0'); ListOfCharacters.push_back('\0'); list<char>::iterator PositionOfNulls = search(ListOfCharacters.begin(), ListOfCharacters.end(), TargetCharacters.begin(), TargetCharacters.end()); if (PositionOfNulls!=ListOfCharacters.end()) cout << "We found the nulls" << endl; }
The output of the program will be 这是程序的输出:

We found the nulls

search算法在一个序列中找另一个序列的第一次出现的位置。在这个例子里我们在ListOfCharacters中 找TargetCharacters这个序列的第一次出现,TargetCharacters是包含两个null字符的序列。 search的参数是两个指着查找目标的iterator和两个指着搜索范围的iterators。 因此我们我们在整个的ListOfCharacters的范围内查找TargetCharacters这个list的整个序列。

如果TargetCharacters被发现,search就会返回一个指着ListOfCharacters中序列匹配的第一个 字符的iterator。如果没有找到匹配项,search返回ListOfCharacters.end()的值。

11 使用list的成员函数sort()排序一个list。


要排序一个list,我们要用list的成员函数sort(),而不是通用算法sort()。所有我们用过的算法都是 通用算法。然而,在STL中有时容器支持它自己对一个特殊算法的实现,这通常是为了提高性能。

在这个例子中,list容器有它自己的sort算法,这是因为通用算法仅能为那些提供随机存取里面元素 的容器排序,而由于list是作为一个连接的链表实现的,它不支持对它里面的元素随机存取。所以就需要一个特殊的 sort()成员函数来排序list。

由于各种原因,容器在性能需要较高或有特殊效果需求的场合支持外部函数(extra functions), 这通过利用构造函数的结构特性可以作到。

/*
|| How to sort an STL list
*/
#include <string>
#include <list>
#include <algorithm>
#include <iostream>
using namespace std;
 
PrintIt (string& StringToPrint) { cout << StringToPrint << endl;}
 
void main (void) {
    list<string> Staff;
    list<string>::iterator PeopleIterator;
 
    Staff.push_back("John");
    Staff.push_back("Bill");
    Staff.push_back("Tony");
    Staff.push_back("Fidel");
    Staff.push_back("Nelson");
 
    cout << "The unsorted list " << endl;
    for_each(Staff.begin(), Staff.end(), PrintIt) ;
 
    Staff.sort();
 
    cout << "The sorted list " << endl;
    for_each(Staff.begin(), Staff.end(), PrintIt);
}

输出是:

The unsorted list
John
Bill
Tony
Fidel
Nelson
The sorted list
Bill
Fidel
John
Nelson
Tony

12 用list的成员函数插入元素到list中


list的成员函数push_front()和push_back()分别把元素加入到list的前面和后面。你可以使用insert() 把对象插入到list中的任何地方。

insert()可以加入一个对象,一个对象的若干份拷贝,或者一个范围以内的对象。这里是一些 插入对象到list中的例子:

/*
|| Using insert to insert elements into a list.
*/
#include <string>
#include <list>
#include <algorithm>
#include <iostream>
using namespace std; void main (void) { list<int> list1; list<int> ::iterator List1Itetator; /* || Put integers 0 to 9 in the list */ for (int i = 0; i < 10; ++i) list1.push_back(i); /* || Insert -1 using the insert member function || Our list will contain -1,0,1,2,3,4,5,6,7,8,9 */ list1.insert(list1.begin(), -1); /* || Insert an element at the end using insert || Our list will contain -1,0,1,2,3,4,5,6,7,8,9,10 */ list1.insert(list1.end(), 10); /* || Inserting a range from another container || Our list will contain -1,0,1,2,3,4,5,6,7,8,9,10,11,12 */ int IntArray[2] = {11,12}; list1.insert(list1.end(), &IntArray[0], &IntArray[2]); /* || As an exercise put the code in here to print the lists! || Hint: use PrintIt and accept an interger */
     //output
  for(List1Itetator=list1.begin();List1Itetator!=list1.end();
     List1Itetator++)
   {
    cout<<*List1Itetator<<endl;
   } }

注意,insert()函数把一个或若干个元素插入到你指出的iterator的位置。你的元素将出现在 iterator指出的位置以前。

13 List 构造函数


我们已经象这样定义了list:

list<int> Fred;

你也可以象这样定义一个list,并同时初始化它的元素:

// define a list of 10 elements and initialise them all to 0
list<int> Fred(10, 0);
// list now contains 0,0,0,0,0,0,0,0,0,0
或者你可以定义一个list并用另一个STL容器的一个范围来初始化它,这个STL容器不一定是一个list, 仅仅需要是元素类型相同的的容器就可以。
vector<int> Harry;
Harry.push_back(1);
Harry.push_back(2);
#
// define a list and initialise it with the elements in Harry
list<int> Bill(Harry.begin(), Harry.end());
// Bill now contains 1,2

14 使用list成员函数从list中删除元素


list成员函数pop_front()删掉list中的第一个元素,pop_back()删掉最后一个元素。 函数erase()删掉由一个iterator指出的元素。还有另一个erase()函数可以删掉一个范围的元素。
/*
|| Erasing objects from a list
*/
#include <list>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std; void main (void) { list<int> list1; // define a list of integers /* || Put some numbers in the list || It now contains 0,1,2,3,4,5,6,7,8,9 */ for (int i = 0; i < 10; ++i) list1.push_back(i); list1.pop_front(); // erase the first element 0 list1.pop_back(); // erase the last element 9 list1.erase(list1.begin()); // erase the first element (1) using an iterator list1.erase(list1.begin(), list1.end()); // erase all the remaining elements cout << "list contains " << list1.size() << " elements" << endl; }
输出是:

list contains 0 elements

15 用list成员函数remove()从list中删除元素。


list的成员函数remove()用来从list中删除元素。
/*
|| Using the list member function remove to remove elements
*/
#include <string>
#include <list>
#include <algorithm>
#include<iostream>
using namespace std;

PrintIt (const string& StringToPrint) {
    cout << StringToPrint << endl;
}
 
void main (void) {
    list<string> Birds;
 
    Birds.push_back("cockatoo");
    Birds.push_back("galah");
    Birds.push_back("cockatoo");
    Birds.push_back("rosella");
    Birds.push_back("corella");
 
    cout << "Original list with cockatoos" << endl;
    for_each(Birds.begin(), Birds.end(), PrintIt);
 
    Birds.remove("cockatoo");
 
    cout << "Now no cockatoos" << endl;
    for_each(Birds.begin(), Birds.end(), PrintIt);
}

输出是:

Original list with cockatoos
cockatoo
galah
cockatoo
rosella
corella
Now no cockatoos
galah
rosella
corella

16 使用STL通用算法remove()从list中删除元素


通用算法remove()使用和list的成员函数不同的方式工作。一般情况下不改变容器的大小。
 
/*
|| Using the generic remove algorithm to remove list elements
*/
#include <string>
#include <list>
#include <algorithm>
#include <iostream>
using namespace std;
 
PrintIt(string& AString) { cout << AString << endl; }
 
void main (void) {
    list<string> Birds;
    list<string>::iterator NewEnd;
 
    Birds.push_back("cockatoo");
    Birds.push_back("galah");
    Birds.push_back("cockatoo");
    Birds.push_back("rosella");
    Birds.push_back("king parrot");
 
    cout << "Original list" << endl;
    for_each(Birds.begin(), Birds.end(), PrintIt);
 
    NewEnd = remove(Birds.begin(), Birds.end(), "cockatoo");
 
    cout << endl << "List according to new past the end iterator" << endl;
    for_each(Birds.begin(), NewEnd, PrintIt);
 
    cout << endl << "Original list now. Care required!" << endl;
    for_each(Birds.begin(), Birds.end(), PrintIt);//zbf.不能用此种方法打印
}

输出结果将为:
Original list
cockatoo
galah
cockatoo
rosella
king parrot


List according to new past the end iterator
galah
rosella
king parrot


Original list now. Care required!
galah
rosella
king parrot
rosella
king parrot
通用remove()算法返回一个指向新的list的结尾的iterator。从开始到这个新的结尾(不含新结尾元素)的范围 包含了remove后剩下所有元素。你可以用list成员函数erase函数来删除从新结尾到老结尾的部分。

17 使用STL通用算法stable_partition()和list成员函数splice()来划分一个list


  我们将完成一个稍微有点复杂的例子。它演示STL通用算法stable_partition()算法和一个list成员函数 splice()的变化。注意函数对象的使用和没有使用循环。 通过简单的语句调用STL算法来控制。

stable_partition()是一个有趣的函数。它重新排列元素,使得满足指定条件的元素排在 不满足条件的元素前面。它维持着两组元素的顺序关系。

splice 把另一个list中的元素结合到一个list中。它从源list中删除元素。

在这个例子中,我们想从命令行接收一些标志和四个文件名。文件名必须’按顺序出现。通过使用stable_partition() 我们可以接收和文件名混为任何位置的标志,并且不打乱文件名的顺序就把它们放到一起。

由于记数和查找算法都很易用,我们调用这些算法来决定哪个标志被设置而哪个标志未被设置。 我发现容器用来管理少量的象这样的动态数据。

/*
|| Using the STL stable_partition algorithm
|| Takes any number of flags on the command line and
|| four filenames in order.
*/
#include <string>
#include <list>
#include <algorithm>
#include <iostream>
using namespace std;
PrintIt ( string& AString { cout << AString << endl; }
 
class IsAFlag {
public:
    bool operator () (string& PossibleFlag) {
        return PossibleFlag.substr(0,1)=="-";
    }
};
 
class IsAFileName {
public:
    bool operator () (string& StringToCheck) {
        return !IsAFlag()(StringToCheck);
    }
};
 
class IsHelpFlag {
public:
    bool operator () (string& PossibleHelpFlag) {
        return PossibleHelpFlag=="-h";
    }
};
 
void main (int argc, char *argv[]) {
 
    list<string> CmdLineParameters; // the command line parameters
    list<string>::iterator StartOfFiles; // start of filenames
    list<string> Flags; // list of flags
    list<string> FileNames; // list of filenames
 
    for (int i = 0; i < argc; ++i) CmdLineParameters.push_back(argv[i]);
 
        CmdLineParameters.pop_front(); // we don't want the program name
 
    // make sure we have the four mandatory file names
    int NumberOfFiles(0);
    count_if(CmdLineParameters.begin(), CmdLineParameters.end(), IsAFileName(), NumberOfFiles);
 
    cout << "The " << (NumberOfFiles == 4 ? "correct " : "wrong ") << "number (" << NumberOfFiles << ") of file names were specified" << endl;
 
 // move any flags to the beginning
    StartOfFiles = stable_partition(CmdLineParameters.begin(), CmdLineParameters.end(), IsAFlag());
 
    cout << "Command line parameters after stable partition" << endl;
    for_each(CmdLineParameters.begin(), CmdLineParameters.end(), PrintIt);
 
    // Splice any flags from the original CmdLineParameters list into Flags list.
    Flags.splice(Flags.begin(), CmdLineParameters, CmdLineParameters.begin(), StartOfFiles);
 
    if (!Flags.empty()) {
        cout << "Flags specified were:" << endl;
        for_each(Flags.begin(), Flags.end(), PrintIt);
    }
    else {
        cout << "No flags were specified" << endl;
    }
 
    // parameters list now contains only filenames. Splice them into FileNames list.
    FileNames.splice(FileNames.begin(), CmdLineParameters, CmdLineParameters.begin(), CmdLineParameters.end());
 
    if (!FileNames.empty()) {
        cout << "Files specified (in order) were:" << endl;
        for_each(FileNames.begin(), FileNames.end(), PrintIt);
    }
    else {
        cout << "No files were specified" << endl;
    }
 
    // check if the help flag was specified
    if (find_if(Flags.begin(), Flags.end(), IsHelpFlag())!=Flags.end()) {
        cout << "The help flag was specified" << endl;
    }
 
    // open the files and do whatever you do
 
}

给出这样的命令行:

//zbf:在vc6IDE下可以模拟出一样的结果

test17 -w linux -o is -w great

输出是:

The wrong number (3) of file names were specified
Command line parameters after stable partition
-w
-o
-w
linux
is
great
Flags specified were:
-w
-o
-w
Files specified (in order) were:
linux
is
great

18 结论

  我们仅仅简单的谈了谈你可以用list做的事情。我们没有说明一个对象的用户定义类,虽然这个不难。

  如果你懂了刚才说的这些算法背后的概念,那么你使用剩下的那些算法就应该没有问题了。使用STL 最重要的东西就是得到基本理论。

  STL的关键实际上是iterator。STL算法作为参数使用iterator,他们指出一个范围,有时是一个范围, 有时是两个。STL容器支持iterator,这就是为什么我们说 list<int>::iterator, 或 list<char>::iterator, 或 list<string>::iterator.

  iterator有很好的定义继承性。它们非常有用。某些iterator仅支持对一个容器只读,某些 仅支持写,还有一些仅能向前指,有一些是双向的。有一些iterator支持对一个容器的随机存取。

  STL算法需要某个iterator作为“动力” 如果一个容器不提供iterator作为“动力”,那么这个算法将无法编译。例如,list容器仅提供双向的 iterator。通常的sort()算法需要随机存取的iterator。这就是为什么我们需要一个特别的list成员函数sort()。

  要合适的实际使用STL,你需要仔细学习各种不同的iterator。你需要知道每种容器都支持那类iterator。 你还需要知道算法需要那种iterator,你当然也需要懂得你可以有那种iterator。

在field中使用STL

  去年,我曾用STL写过几个商业程序。它在很多方面减少了我的工作量,也排除了很多逻辑错误。

  最大的一个程序有大约5000行。可能最惊人的事情就是它的速度。它读入并处理一个1-2兆的 报告文件仅花大约20秒。我是在linux上用gcc2.7.2开发的,现在运行在HP-UX机器上。 它一共用了大约50和函数对象和很多容器,这些容器的大小从小list到一个有14,000个元素的map都有。

  一个程序中的函数对象是处于一个继承树中,顶层的函数对象调用低层的函数对象。我大量的使用STL算法for_each() ,find(),find_if(),count()和count_if(),我尽量减少使用程序内部的函数,而使用STL的算法调用。

  STL倾向于自动的把代码组织成清晰的控制和支持模块。通过小心使用函数对象并给它们 起有意义的名字,我使它们在我的软件的控制流中流动。

  还有很多关于STL编程要知道的东西,我希望你通过这些例子可以愉快的工作。

  参考数目中的两本书在web上都有勘误表,你可以自己改正它们。

  Stroustrup在每一章后面都有个建议栏,特别是对于出学者有用。正本书比早期的版本更加健谈。 它也更大了。书店里还可以找到其他几本关于STL的教科书。去看看,也许你能发现什么。

原文地址:https://www.cnblogs.com/zjz/p/507299.html