提供了类型安全、高效而易用特性的STL无疑是最值得C++程序员骄傲的部分。每一个C++程序员都应该好好学习STL:).
STL(Standard Template Library 标准模板库)是C++标准库的一个重要组成部分,它由Stepanov and Lee等人最先开发,它是与C++几乎同时开始开发的;一开始STL选择了Ada作为实现语言,但Ada有点不争气,最后他们选择了C++,一个原因了,C++中已经有了模板。在后来,STL又被添加进了C++库。1996年,惠普公司又免费公开了STL,为STL的推广做了很大的贡献。
STL大体上包括container(容器)、algorithm(算法)和iterator(迭代器),容器和算法通过迭代器可以进行无缝连接。
STL体现了范型编程的思想,它具有高度的可重用性,高性能,高移植性。程序员不用思考具体实现过程,只要能够熟练的应用就OK了。(有兴趣研究具体实现的,可以看侯捷老师编著的《STL源码剖析》)这样他们就可以把精力放在程序开发的别的方面。
我非常佩服创造STL的那些计算机和数学精英。因为他们做了一件非常辛苦的事情―――抽象概念。而STL就是通过把容器抽象为统一的接口,算法利用这个接口,通过迭代器来操纵容器。因为接口不变,实现的容器可以随意更改。这样,就为编程、调试和扩展提供了便利。也许有一天,我们生产软件的时候也可以想DIY一台PC一样简单,只要拿来相应的实现模块,通过简单的拼装和调试就可以创造一个软件。这是多么令人兴奋的一件事^_^.不过,到时候,可能会有很多程序员失业了。呵呵,毕竟编写类库不需要很多的人员。
虽然STL不是面向对象的,但,我想,每个人都会为它的创造力和高性能而感到兴奋和折服。
一、容器
作为STL的最主要组成部分--容器,分为向量(vector),双端队列(deque),表(list),队列(queue),堆栈(stack),集合(set),多重集合(multiset),映射(map),多重映射(multimap)。
容器 |
特性 |
所在头文件 |
向量vector |
可以用常数时间访问和修改任意元素,在序列尾部进行插入和删除时,具有常数时间复杂度,对任意项的插入和删除就有的时间复杂度与到末尾的距离成正比,尤其对向量头的添加和删除的代价是惊人的高的 |
<vector> |
双端队列deque |
基本上与向量相同,唯一的不同是,其在序列头部插入和删除操作也具有常量时间复杂度 |
<deque> |
表list |
对任意元素的访问与对两端的距离成正比,但对某个位置上插入和删除一个项的花费为常数时间。 |
<list> |
队列queue |
插入只可以在尾部进行,删除、检索和修改只允许从头部进行。按照先进先出的原则。 |
<queue> |
堆栈stack |
堆栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最近插入序列的项。即按照后进先出的原则 |
<stack> |
集合set |
由节点组成的红黑树,每个节点都包含着一个元素,节点之间以某种作用于元素对的谓词排列,没有两个不同的元素能够拥有相同的次序,具有快速查找的功能。但是它是以牺牲插入车删除操作的效率为代价的 |
<set> |
多重集合multiset |
和集合基本相同,但可以支持重复元素具有快速查找能力 |
<set> |
映射map |
由{键,值}对组成的集合,以某种作用于键对上的谓词排列。具有快速查找能力 |
<map> |
多重集合multimap |
比起映射,一个键可以对应多了值。具有快速查找能力 |
<map> |
考虑到不同的实际需要,更主要的是效率的需要,我们可以选择不同的容器来实现我们的程序,以此达到我们提高性能的目的。这也是用好STL的一个难点,但这也是关键。
二、算法
算法部分主要由头文件<algorithm>,<numeric>和<functional>组成。<algorithm>是所有STL头文件中最大的一个,它是由一大堆模版函数组成的,可以认为每个函数在很大程度上都是独立的,其中常用到的功能范围涉及到比较、交换、查找、遍历操作、复制、修改、移除、反转、排序、合并等等。<numeric>体积很小,只包括几个在序列上面进行简单数学运算的模板函数,包括加法和乘法在序列上的一些操作。<functional>中则定义了一些模板类,用以声明函数对象。
STL的算法也是非常优秀的,它们大部分都是类属的,基本上都用到了C++的模板来实现,这样,很多相似的函数就不用自己写了,只要用函数模板就OK了。
我们使用算法的时候,要针对不同的容器,比如:对集合的查找,最好不要用通用函数find(),它对集合使用的时候,性能非常的差,最好用集合自带的find()函数,它针对了集合进行了优化,性能非常的高。
三、迭代器
它的具体实现在<itertator> 中,我们完全可以不管迭代器类是怎么实现的,大多数的时候,把它理解为指针是没有问题的(指针是迭代器的一个特例,它也属于迭代器),但是,决不能完全这么做。
迭代器功能(Abilities Of IteratorGategories) |
||
输入迭代器 Input iterator |
向前读 Reads forward |
istream |
输出迭代器 Output iterator |
向前写 Writes forward |
ostream,inserter |
前向迭代器 Forward iterator |
向前读写 Read and Writes forward |
|
双向迭代器 Bidirectional iterator |
向前向后读写 Read and Writes forward and backward |
list,set,multiset,map,mul timap |
随机迭代器 Random access iterator |
随机读写 Read and Write with random access |
vector,deque,array,string |
由此可见,指针和迭代器还是有很大差别的。和指针最接近的就是随机访问迭代器。下面是一个我编写的小例子:功能是分别对数组,向量,表,多重集合进行插入操作,对每个容器插入100万个随机整数;
#include<iostream>
#include<iterator> #include<vector> #include<list> #include< set> #include<time.h> #include<conio.h> #include<algorithm> using namespace std; template<typename T> void arrayInsert(T*a,T*s, long size) // 向数组插入数据 { //for(long i=0;i<10;i++) // //好像数组支持不到100万,我们就算10万的 //最后在把把结果乘以10吧, //{ for(long k=0;k<size;k++) { a[k]=s[k]; } //} } template<typename T> void vectorInsert( vector<T> *v,T*s, long size) // 向向量插入数据 { for(int i=0;i<10;i++) { for(long k=0;k<size;k++) { v->push_back(s[k]); } } } template<typename T> void listInsert(list<T>*l,T*s, long size) // 向表插入数据 { for(int i=0;i<10;i++) { for(long k=0;k<size;k++) { l->push_back(s[k]); } } } template< class T> void multisetInsert(multiset<T>*s1,T*s, long size) // 向多重集合插入数据 { for(int i=0;i<10;i++) { for(long k=0;k<size;k++) { s1->insert(s[k]); } } } int* genIntData( long size) // 生成随机数 { int* data=new int[size]; generate(&data[0],&data[size],rand); return data; } int main( void) { const long size=100000; int* s_data,array1[size]; double begin,end; s_data=genIntData(size); vector<int> vector1; list<int> list1; multiset<int> multiset1; clock(); cout<<"?"<<endl; begin=(double)clock()/CLK_TCK; arrayInsert<int>(array1,s_data,size); end=(double)clock()/CLK_TCK; cout<<"??"<<(end-begin)<<endl; getch(); begin=(double)clock()/CLK_TCK; vectorInsert<int>(&vector1,s_data,size); end=(double)clock()/CLK_TCK; cout<<"??"<<(end-begin)<<endl; getch(); begin=(double)clock()/CLK_TCK; listInsert<int>(&list1,s_data,size); end=(double)clock()/CLK_TCK; cout<<"??"<<(end-begin)<<endl; getch(); begin=(double)clock()/CLK_TCK; multisetInsert<int>(&multiset1,s_data,size); end=(double)clock()/CLK_TCK; cout<<"??"<<(end-begin)<<endl; getch(); free(s_data); return 0; } |
这个程序清晰的表明这几种容器在插入速度之间的差别,当然,每种容器不是万能的,不能一好百好。比如说,多集在查找方面的优势是其他序列容器不可比拟的。 还有,最好不要试图超越STL,因为:
1、STL实现使用的是最佳算法。它是几乎完美的。
2、STL实现的设计者通常是相应领域的专家。
3、各领域的专家致力于提供灵活的、强大和高效的库。这是他们的首要的任务。对于,我们这些其余的人,开发可重用的容器和算法顶多只算第二个目标。我们的首要任务是交付紧扣主题的应用程序。大多数情况下,我们没有时间和专门的技术去和那些专家相比。
但是,超越STL不是不可能的。但是一般情况下,你只能靠牺牲可移植性来提高性能,这对于很多情况来说是很不好的。为了,超越STL,我们要付出非常大的努力。而且,最好我们知道一些STL专家们不知道的东西,尔后我们可以有针对性的进行优化,否则,我们的努力完全有可能白费。
面对这样一个优秀的库,并且它是免费的。我们C++程序员没有理由拒绝它,甚至去自己开发一个库。