STL是一个标准规范,它只是为容器、迭代器和泛型算法等组件定义了一整套统一的上层访问接口及各种组件之间搭配运用的一般规则,而没有定义组件底层的具体实现方法。
STL主要包括下面这些组件:I/O流,string类、容器类(Container)、迭代器(Iterator)、存储分配器(Allocator)、适配器(Adapter)、函数对象(Functor)、泛型算法(Algorithm)、数值运算、国际化和本地化支持、以及标准异常类等。
其中最重要的组件是:容器、存储分配器、迭代器、泛型算法、函数对象和适配器,俗称“六大组件”.
一、STL头文件分布
只要有一系列元素构成的结构原则上都可以应用泛型算法,像C++/C数组、字符串、I/O流等特殊的容器也可以使用某些泛型算法--它们定义在头文件<algorithm>和<utility>.
迭代器就是用来遍历元素序列或元素集合的“通用指针”,但是每一种容器都定义了合适自己使用的迭代器,那些具有特殊功能的迭代器,如输入/输出迭代器,插入迭代器,反向迭代器等都是迭代器适配器,定义在头文件<iterator>.
二、容器设计原理
容器对象:容器本身也是C++类的对象,例如std::vector<T>
容器元素对象:指容器对象内存储的数据元素,可以是内置类型的对象,也可以是自定义数据类型的对象。
STL容器的实现方式可以归纳为下图:
STL主要采用向量、链表、二叉树及它们的组合作为底层存储结构来实现容器。
存储方式和访问方式
向量(vector)和链表是两种最基本的动态结构,也是STL中两种最基本的容器,分别对应动态数组和链表结构,同时它们分别代表了内存中同类型批量数据存放的两种基本方式:连续存储和随机存储。
由此决定了不同的访问方式:随机访问和顺序访问。
C++/C的内置数组和vector都是既可以随机访问又可以顺序访问的容器,而list则只能顺序访问。
vector内存映像如下:
list的内存映像
只要底层存储机制采取连续存储方式的容器,就可以随机访问其中任一元素对象,否则只能顺序访问;而任何容器都可以顺序访问,及遍历。
stack、queue及priority_queue在概念和接口上都不支持随机访问和遍历,这是由它们的语义决定的,而不是由底层存储方式决定的,因此没有迭代器(所以它们才被叫做容器适配器而不是归为容器类)。
树,在本质上是一种特殊的链表结构,因此只能顺序访问,即从某个节点开始搜索直至到达所要访问的元素对象,或者采用深度优先、广度优先或者前序、中序、后序等方法遍历整颗树,但是不可能直接定位到树上的任一个节点对象。
顺序容器和关联式容器的比较
这里的“顺序”和“关联”指的是上层接口表现出来的访问方式,并非底层存储方式。
顺序容器主要采用向量和链表及其组合作为基本存储结构,如堆栈和各种队列。
关联式容器采用平衡二叉搜索树作为底层存储结构。红黑树是平衡二叉搜索树的一种,其在元素定位上的性能优异(O(log₂N)),STL通常用来实现关联式容器。
链表(list)作为顺序容器的典型代表,其特点就是“天然有序”,即各元素之间具有天然的相对位置关系,这就决定了理论上它可以存储任意多个元素,且不需要对所有元素按值的大小进行排序。
集合(set)是关联式容器的典型代表,需要从两个层次来理解:概念模型和实现方式。
在使用上的区别:
如何遍历容器?
从图17-2和17-4看出STL的有效元素的表示范围:
迭代器last要么指向最后一个有效元素的末尾,要么指向一个空白节点,反正不是指向最后一个有效元素。这就是STL容器的“前闭后开”区间法,即[first,last).
上图17-5的存储结构可以发现,它与list有些共同之处,例如使用一个空白节点来表示end(),而且begin()指向最左端节点,end()指向开头的空白节点而不是指向最右端节点。这就是说关联式容器采用的是二叉树的“中序遍历”,按照默认升序排列,访问结构是:5,10,15,20,23,25,30.