STL序列容器(一)
摘要
C++ STL是C++ 标准库中及其重要的一部分,对C++ STL的使用是每一位 C++ 程序员都应该熟悉的,在实际开发中,利用好这些前人造的轮子,能够提高开发效率且减少出错的概率。我个人认为C++ STL最重要的内容分为两块,其一是容器类,其二是算法类。本篇文章对STL容器类中的序列容器进行介绍,主要涉及内部实现、常用接口。当然所谓的内部实现是很早版本的STL容器的实现,随着标准库的不断更新,实现更加高效、安全,但是很多内部使用的数据结构和实现思想是没有改变的。了解这些内部实现的目的是更好的使用这些容器,知道何种容器的何种操作耗时等。
本篇文章对STL容器中的部分序列容器进行介绍,主要参考资料为侯捷老师的《STL源码剖析》,其它相关的参考链接在文末列出。
STL六大组件
C++ STL主要分为六大组件,分别是容器(container),算法(algorithm),迭代器(iterator),仿函数(functors),适配器(adapters),配置器(allocators) 。
它们之间的相互关系如下:
上图中六大组件:
- 容器(container) :各种数据结构,如vector、list、deque、set、map等,是一种class template,用来存放数据。
- 算法(algorithm) :各种常用算法如:sort、search、copy等,是一种function template。
- 迭代器(iterator) :作为容器和算法之间的桥梁,是所谓的泛型指针。从实现的角度看,它是重载了oeprator*,operator->,operator++,operator--等运算符的class template。 每种容器都有相应的迭代器。
- 仿函数(functors) :行为类似函数,从实现的角度来看是实现了operator()的class,函数指针也可以看作是狭义的仿函数functor。
- 适配器(adapters) :一种用于修饰容器或仿函数或迭代器的东西,例如stack和queue,虽然看似容器,内部完全借助deque进行实现,只能看作是一种adapter。
- 配置器(allocators) :负责空间的分配和管理,是实现了动态空间配置和管理的class template。
std::vector
概述
vector为可变大小数组,支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢。实际开发中,选择容器通常来说使用vector是最好的选择,除非你有很好的理由选择其他容器。vector提供的迭代器类型为Random Access Iterators,这种迭代器支持随机访问元素,即支持自加、自减和加减常数等操作。
vector的声明如下:
1 | template <class T, class Alloc = allocator<T> > class vector; |
容器模板中两个参数分别是:
- T:容器中元素类型,可以用
vector::value_type
访问。 - Alloc:空间分配和释放时使用的分配器(allocator),默认使用
allocator<T>
分配器进行空间分配,一般不需要指定。
就像数组一样,vector内部使用连续的内存空间存储元素,不同的是它的大小可以改变。 其基本内部实现为:首先预先分配一定大小(假设为n个element的大小)的连续空间,当容器不断添加元素使得容器元素个数大于n时,vector会进行空间的重新分配,例如重新找到一片大小为 2n 个 element 的连续空间,将原空间的所有元素拷贝至新空间,并继续添加新元素。这个重新分配并拷贝的过程是很慢的。
这个过程如下图(当然2倍空间增长只是其中一种实现方法,标准库不对实现方式进行规定):
常用接口
这里只列举部分常用的接口函数,完整接口参考 http://www.cplusplus.com/reference/
- 构造函数(省略了拷贝和移动构造函数)
1 | //default (1) |
实例:
1 | int main () |
- 大小
1 | size_type size() const noexcept; //当前存储的元素个数 |
- 添加/删除元素
1 | void push_back (const value_type& val); //在vector末尾添加元素 |
- 获取元素或迭代器
1 | //除以下之外,还有反向迭代器未列出 |
std::list
概述
list是具有常数插入和删除元素操作的顺序容器,其内部实现为双向链表,有些实现还是环状双向链表。list的最大缺点是它不支持直接索引,为了访问链表中的第6个元素,必须从已知的位置(例如头迭代器或尾迭代器)逐个地访问过去,这消耗的时间的线性的。list提供的迭代器类型为Bidirectional Iterators,这种迭代器支持自加、自减操作,但是不能够加或减去常数,即只允许迭代器指针单个元素移动,而不能够一次性跨越多个元素。
list的声明和vector一致:
1 | template < class T, class Alloc = allocator<T> > class list; |
下图是双向循环链表结构:
常用接口
- 构造函数(和vector基本一致)
1 | explicit list (const allocator_type& alloc = allocator_type()); |
- 大小
1 | size_type size() const noexcept; |
- 添加/删除元素
1 | void push_back (const value_type& val); |
- 获取元素或迭代器(list不支持下标访问)
1 | reference back(); |
- 其它操作
1 | void splice (const_iterator position, list& x); //将list x中的元素转移至position指定的位置,这同时改变了两个list的大小,且不涉及任何元素的构造和析构,这就是元素的位置转移。 |
std::deque
概述
vector是单向开口的连续线性空间,deque则是一种双向开口的序列容器,它也具有动态大小,且可以在两端进行扩充或缩减。vector实际上也可以在头部进行插入和删除,但是效率非常差(需要将其他元素都挪位置),而deque能够在两端进行常数时间的插入和删除。deque也提供Random Access Iterator。但是deque的迭代器并不是普通的指针,它的复杂度比vector迭代器高多了。
其声明为:
1 | template < class T, class Alloc = allocator<T> > class deque; |
为了实现双向插入和删除,deque内部结构较为复杂,其内部并不像vector一样是完全的连续空间。首先deque内部存在一个“buffer指针图”命名为map,当然并不是STL中的map,其实质是一段连续的内存空间,这段空间中保存了若干个指针,这些指针分别又指向一段固定长度的连续内存空间(buffer) 。
其迭代器保存了4个指针,分别是cur
, first
, last
, node
。其中cur
指向实际数据,first
指向实际数据所处的那一个buffer的第一个元素,last
指向buffer的最后一个元素末尾,node
保存了该buffer在指针图map中的位置。
从头部添加元素时,是在头迭代器的前一个位置添加;从尾部添加元素时,是在尾迭代器的位置添加。当添加元素的位置buffer满了时,会在map中配置一个新节点,并申请一个新的buffer来存放。
当map头部节点或尾部节点也不够了怎么办?显然当map的头部备用节点或尾部备用节点不够了时,会重新申请一块新的连续内存作为新的map,并将原来的map拷贝过来,就像vector一样。
常用接口
和list接口基本一致。
- 构造函数
1 | //default (1) |
- 大小
1 | size_type size() const noexcept; |
- 添加/删除元素
1 | void push_back (const value_type& val); |
- 获取元素或迭代器
1 | reference back(); |
std::stack
概述
stack是一种 先进后出(FILO) 的数据结构,它允许新增元素、移除元素、取得最顶端的元素,但是除了最顶端外,没有任何地方可以存取stack的其它元素,即stack不允许遍历行为。准确的来说,STL中的stack是一种容器适配器(container adaptors),它只是其他容器的包装,这个其他容器必须提供以下操作:
- empty
- size
- back
- push_back
- pop_back
因此stack的声明如下:
1 | template <class T, class Container = deque<T> > class stack; |
其中模板的第二个参数为容器类型,实际上,标准容器中的vector,deque和list都满足上述条件,可以作为该参数的选项。但是默认的选项是deque。需要说明的是:stack不提供迭代器。
stack和queue的底层实现都非常简单,实际上就是把模板中容器的上述方法进行了包装,基本上就是原封不动的调用。
常用接口
- 构造函数(一般不改变默认容器,直接构造一个空的stack;这里没有列出拷贝和移动构造函数)
1 | explicit stack (const container_type& ctnr); //从一个已有的容器进行构造 |
实例:
1 | // constructing stacks |
- 其它接口
1 | void push (const value_type& val); //在栈顶添加一个元素 |
std::queue
概述
queue是一种先进先出(FIFO)的数据结构,数据只能从一个开口进,从另一个开口出。和stack一样,queue也是一种容器适配器(container adaptors) ,它只是其它容器的包装,这个其它容器必须要可以从back推入元素并从front取出元素,即具备下列操作:
- empty
- size
- front
- back
- push_back
- pop_front
queue的声明如下(和stack一样):
1 | template <class T, class Container = deque<T> > class queue; |
第二个参数为容器类型,标准容器中的deque和list都符合要求。默认使用deque作为底层容器。同样queue不提供迭代器。
常用接口
- 构造函数
1 | explicit queue (const container_type& ctnr); |
实例:
1 | // constructing queues |
- 其他接口
1 | void push (const value_type& val); //从back方向放入一个值,底层调用了容器的push_back方法 |
总结
本篇介绍了STL容器中的std::vercor, std::list, std::deque以及两种容器适配器:std::stack, std::queue。对这些容器的内部数据结构以及常用的接口进行了简要介绍,限于篇幅,STL序列容器中的forward_list, priority_queue放置下一篇文章进行介绍。
参考资料
- 《STL源码剖析》侯捷
- http://www.cplusplus.com/reference/
- 《C++ Primer 5th》