STL,即标准模板库,它涵盖了常用的数据结构和算法,并且具有跨平台的特点。将泛型编程思想和STL库用于系统设计中,明显降低了开发强度,提高了程序的可维护性及代码的可重用性。
STL是C++标准函数库的一部分。STL的基本观念就是把数据和操作分离。
STL中数据由容器来加以管理,操作则由可制定的算法来完成。迭代器在容器和算法之间充当黏合剂,它使得任何算法都可以和任何容器进行交互运作。STL含有容器,算法,迭代器组件。
其中容器分为下面几种。
1)STL序列容器:vector,string,deque和list。
2)STL关联容器:set,multiset,map和multimap。
3)STL适配容器:stack,queue和priority_queue。
1.什么是STL?
STL(standard Template Library),即标准模板库,是一个具有工业强度的,高效的C++程序库。它被容纳与C++标准程序库(C++ Standard Library)中,是ANSI/ISO C++标准中最新的也是极具革命性的一部分。该库包含了诸多在计算机科学领域里所常用的基本数据结构和算法,为广大C++程序员提供了一个可扩展的应用框架,高度体现了软件的复用性。它类似于Microsoft Visual C++中的MFC(Microsoft Foundation Class Library)。
从逻辑层次来看,在STL中体现了泛型化程序设计思想,引入了许多新的名词,比如需求,概念,模型,容器,算法,迭代器等。
从实现层次看,整个STL是以一种类型参数化的方式实现的,这种方式基于一个在早先C++标准中没有出现的语言特性——模板。如果查阅任何一个版本的STL源代码,就会发现,模板作为整个STL的基石是一件千真万确的事情。除此之外,还有许多C++的新特性为STL的实现提供了方便。
STL是最新的C++标准函数库的一个子集,它占据了整个库的大约80%的分量。而作为在实现STL过程中扮演关键角色的模板,则充斥了几乎整个C++标准函数库。C++标准函数库中的各个组件的关系如图。
2.具体说明STL 如何实现vector
vector内部是使用动态数组的方式实现的。如果动态数组的内存不够用,就要动态地重新分配,一般是当前大小的两倍,然后把原数组的内容拷贝过去。所以,在一般情况下,其访问速度同一般数组,只有在重新分配发生时,其性能才会下降。它的内部使用allocator类进行内存管理,程序员不需要自己操作内存。
3.看代码回答问题——vector容器中iterator的使用
vector<int>array;
array.push_back(1);
array.push_back(2);
array.push_back(3);
for(vector<int>::size_type i=array.size()-1;i>=0;--i)//反向遍历array数组
{
cout<<array[i]<<endl;
}
当运行代码时,没有输出3 2 1,而是输出了一大堆很大的数字,为什么?
修改代码:
for(vector<int>::size_type j=array.size()-1;j>=0;j--)
{
cout<<array[j-1]<<endl;
}
这样就输出了3 2 1。为什么呢?
由于vector支持随机访问,并且重载了[]运算符,因此可以像数组那样(比如a[j])来访问vector中的第i+1个元素。
现在来简单分析vector中size_type的定义过程。为方便起见,下面省略了某些头文件。
在vector定义中可以看到:
1 typedef _A::size_type size_type;
而A是alloctor<_Ty>,因此查看alloctor的定义,不难发现:
1 typedef _SIZT size_type;
而_SIZT的定义为:
1 #define _SIZT size_t
最后size_t的定义为:
1 typedef unsigned int size_t;
因此最后的结果为:size_type是个unsigned int类型成员。我们知道,无符号的整数是大于等于0的,因此上面第一段代码(代码中第5行)中的i>=0永远为true,程序一直循环,输出很多随机数,最后程序崩溃。而第二段代码(代码第一行)使用了i>0作为循环的条件,即i为0时结束循环。
4.看代码找错误——vector容器的使用
typedef vector Intarray;
IntArray array;
array.push_back(1);
array.push_back(2);
array.push_back(2);
array.push_back(3);
//删除array数组中所有的2
for(IntArray::iterator itor=array.begin();itor!=array.end();++itor)
{
if(2==*itor)
array.erase(itor);
}
这道题有两个错误。
1)代码第一行没有使用类型参数,这将会导致编译错误,由于array需要添加int类型的元素,因此代码第一行定义vector时因该加上int类型。
1 typedef vector<int> IntArray;
2)代码第8~12行的for循环,这里只能删除array数组中的第一个2,而不能删除所有的2。这是因为,每次调用“array.erase(itor);”后,被删除元素之后的内容会自动往前移,导致迭代漏项,应在删除一项后使itor–;使之从已经前移的下一个元素起继续遍历。
正确的代码如下:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
typedef vector<int> IntArray;
IntArray array;
array.push_back(1);
array.push_back(2);
array.push_back(2);
array.push_back(3);
//删除array中数组中所有的2
for(IntArray::iterator itor=array.begin();itor!=array.end();++itor)
{
if(2==*itor)
{
array.erase(itor);
--itor;//删除一项后使itor--
}
}
//测试删除后array中的内容
for(itor=array.begin();itor!=array.end();++itor)
{
cout<<*itor<<endl;
}
}
5.把一个文件中的整数排序后输出到另一个文件中
(运用vector解决实际问题)
这个题目设计文件操作以及排序。我们可以使用vector容器来简化文件操作。在读文件的时候用push_back把所有的整数放入到一个vector对象中,在写文件时用[]操作符直接把vector对象输出到文件。代码如下:
#include<iostream>
#include<fstream>
#include<vector>
using namespace std;
//对data容器中的所有元素进行vector排序
void Order(vector<int>& data)
{
int count=data.size();//获得vector中的元素个数
for(int i=0;i<count;i++)
{
for(int j=0;j<count-i-1;j++)
{
if(data[j]>data[j+1])//如果当前元素比下一个元素大,则交换
{//结果为升序排列
int temp=data[j];
data[j]=data[j+1];
data[j+1]=temp;
}
}
}
}
int main()
{
vector<int>data;
ifstream in("C:\\data.txt");
if(!in)//打开输出文件失败
{
cout<<"infile error!"<<endl;
return 1;
}
int temp;
while(!in.eof())
{
in>>temp;//从文件中读取整数
data.push_back(temp);//把读取的证书放入data容器中
}
in.close;
Order(data);//冒泡排序
ofstream out("c:\\result.txt");
if(!out)//打开输出文件失败
{
cout<<"outfile error!"<<endl;
return 1;
}
for(int i=0;i<data.size();i++)
cout<<data[i]<<" ";//把data容器中的所有元素输出至文件
out.close();
return 0;
}
6.list和vector有什么区别
vector和数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好地支持随机存取(使用[ ]操作符访问其元素)。但是由于它的内存空间是连续的,所以在中间进行插入和删除操作会造成内存块的拷贝(O(n))。另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。
list是由数据结构中的双向链表实现的,因此它的内存空间可以是不连续的。因此只能通过指针来进行数据的访问。这个特点使得它的随机存取变得非常没有效率,需要遍历中间的元素,搜索复杂度O(n),因此它没有提供[ ]操作符的重载。但由于链表的特点,它可以很好的效率支持任意地方的删除和插入。
由于list和vector上面的这些区别,list::iterator与vector::iterator也有一些不同。例子如下:
#include<iostream>
#include<vector>
#include<list>
using namespace std;
int main(void)
{
vector<int> v;
list<int> l;
for(int i=0;i<8;i++)//往v和l中分别添加元素
{
v.push_back(i);
l.push_back(i);
}
cout<<"v[2]="<<v[2]<<endl;
//cout<<"l[2]="<<l[2]<<endl;//编译错误,list没有重载[]
cout<<(v.begin()<v.end())<<endl;
//cout<<(l.begin()<l.end())<<endl;//编译错误,list::iterator没有重载<或>
cout<<*(v.begin()+1)<<endl;
vector<int>::iterator itv=v.begin();
list<int>::iterator it1=l.begin();
itv=itv+2;
//it1=it1+2;//编译错误,list::iterator没有重载+
it1++;it1++;//list::iterator中重载了++,只能使用++进行迭代访问
cout<<*itv<<endl;
cout<<*it1<<endl;
return 0;
}
由于vector用于一段连续的内存空间,能非常好地支持随机存取,因此vector::iterator支持“+”,“-”,“+=”,“<”等操作符。
而list的内存空间可以是不连续的,它不支持随机访问,因此list::iterator不支持“+”,“+=”,“<”等操作符运算。因此代码第20,26行会有编译错误。只能使用“++”进行迭代,例如代码第27行,使用两次itl++来移动itl。还有list也不支持[ ]运算符,因此代码第18行出现编译错误。
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,就使用vector;如果需要大量的插入和删除,而不关心随机存取,则应使用list。
7.分析代码问题并修正——list和vector容器的使用
(理解vector和list的使用)
#include<iostream>
#include<vector>
#include<list>
using namespace std;
int main(void)
{
list list1;
for(int i=0;i<8;i++)
{
list1.push_back(i);
}
for(list::iterator it=list1.begin();it!=list1.end();++it)
{
if(*it%2==0)
{
list1.erase(it);
}
}
return 0;
}
有两个方面的问题。
第一个问题是list1以及它的iterator都缺少类型参数,代码第8行和第15行都有编译错误。由于在代码第12行添加的元素类型为int,因此需要将第8行和第15行中的list::改成list::。
第二个问题是当改正了上面编译的错误后,运行程序会导致程序崩溃。因为当第一次执行代码第19行(调用erase方法删除元素)后,it原来指向的元素内存已经被释放了,因此进入下一次循环后,获得it的值就出现了访问违规。
这里要注意:
vector使用的是数组方式,当删除一个元素后,此元素的后面元素会自动往前移动。而list由于使用链表结构,因此执行erase时会释放链表的结点内存。但是可以通过erase的返回值获得原来链表的下一个元素。
改正后代码如下:
#include<iostream>
#include<vector>
#include<list>
using namespace std;
int main(void)
{
list<int> list1;//指定存放int类型的元素
for(int i=0;i<8;i++)
{
list1.push_back(i);//把整数放入list1中
}
//iterator也需要指明是int类型元素的迭代器
for(list<int>::iterator it=list1.begin();it!=list1.end();++it)
{
if(*it%2==0)
{
list1.erase(it);//得到下一个元素的位置
it--;//回到上一个元素位置
}
}
for(it=list1.begin();it!=list1.end();++it)
{
cout<<*it<<endl;//输出list1内的各个元素
return 0;
}
执行了代码第10~13行,0-8这9个数字被放入list1中。这里简单分析一下删除过程。当it指向第一个元素,即0时,由于0是2的倍数,因此18行的if判断为真,执行删除操作,it返回下一个元素位置,即指向原来的1。为了让进入下一下循环时it还指向1,需要让it–(不能使用it=it-1)。
8.stl::deque是一种什么数据类型
(理解deque容器的内部结构)
deque是由一段一段定量的连续空间组成的,因此是动态数组类型。