前言
deque
容器比list
以及vector
容器都要复杂的多。它用两端都有开口的线性空间来存储数据,这样的话在首部操作数据的效率比vector
高,以及虽然它的迭代器是random_access_iterator_tag
型的,但是并不是原生指针,需要封装,比list
和vector
的迭代器都要复杂。它实现的机制是利用二级指针,维护一个指针数组map
,然后数组中存储的指针指向具体的线性空间。
map
并不是从开头依次使用,而是先选择二级指针数组中间的部分使用,这是为了deque
能够前后都加入等量的元素。我们把二级指针数组称为deque
的中控器,通过中控器得到真正存储元素的空间。
deque
的中控器
首先我们先简单介绍一下map
相关的知识,即定义部分,这样有利于我们后面对迭代器等知识的分析。
这里先列出部分源码:
//可以看到多了一个缺省参数BufSiz=0
//具体有什么作用,我们看到后面再说
template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
public: // Basic types
//一些常用类型的声明
typedef T value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
public: // Iterators
//迭代器的声明
#ifndef __STL_NON_TYPE_TMPL_PARAM_BUG
typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
typedef __deque_iterator<T, const T&, const T&, BufSiz> const_iterator;
#else /* __STL_NON_TYPE_TMPL_PARAM_BUG */
typedef __deque_iterator<T, T&, T*> iterator;
typedef __deque_iterator<T, const T&, const T*> const_iterator;
#endif /* __STL_NON_TYPE_TMPL_PARAM_BUG */
...
protected: // Internal typedefs
/* map_pointer的类型即T **
* 它先指向指针数组,指针数组里面的指针再指向存储元素的线性空间(即结点)
*/
typedef pointer* map_pointer;
/* 这里需要注意一下
* deque容器有两个空间配置器
* 一个用于配置元素
* 另一个用于配置指针数组
*/
typedef simple_alloc<value_type, Alloc> data_allocator;
typedef simple_alloc<pointer, Alloc> map_allocator;
使用这样的设计需要注意到几个问题。虽然每一个指针指向的空间是连续的,但是当一段数据横跨多个指针指向的连续空间时,就需要指针的切换了。如果要维持这种连续空间的假象,则需要考虑到每个线性空间跳到下一个线性空间的边界情况。
以及当指针数组不够大时需要再继续分配的情况。
为了支持这些操作,deque
的迭代器确实要复杂一些。下面我们就来看看deque
的迭代器源码。
deque
的迭代器
大概有100多行的样子,deque
的迭代器除了要取元素的值这些操作以外,还需要知道指针指向的一段线性空间的首尾,不然++
或者--
操作的时候,无法跳转到另外一段线性空间。
template <class T, class Ref, class Ptr, size_t BufSiz>
struct __deque_iterator {
//声明迭代器
typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
typedef __deque_iterator<T, const T&, const T*, BufSiz> const_iterator;
static size_t buffer_size() {return __deque_buf_size(BufSiz, sizeof(T)); }
#else /* __STL_NON_TYPE_TMPL_PARAM_BUG */
template <class T, class Ref, class Ptr>
struct __deque_iterator {
typedef __deque_iterator<T, T&, T*> iterator;
typedef __deque_iterator<T, const T&, const T*> const_iterator;
static size_t buffer_size() {return __deque_buf_size(0, sizeof(T)); }
#endif
//声明的5个相应型别及size_type
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
//map_pointer指针
typedef T** map_pointer;
typedef __deque_iterator self;
/* 三个内部指针
* cur指向当前元素
* first指向当前元素所在的线性空间的首部
* last指向当前元素所在的线性空间的尾部
*/
T* cur;
T* first;
T* last;
/* 二级指针,指向指针数组 */
map_pointer node;
//迭代器的构造函数
__deque_iterator(T* x, map_pointer y)
: cur(x), first(*y), last(*y + buffer_size()), node(y) {}
__deque_iterator() : cur(0), first(0), last(0), node(0) {}
__deque_iterator(const iterator& x)
: cur(x.cur), first(x.first), last(x.last), node(x.node) {}
/* 重载操作符*和->
* 返回cur指向的元素的值以及地址
*/
reference operator*() const { return *cur; }
#ifndef __SGI_STL_NO_ARROW_OPERATOR
pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */
/* 重载操作符-
* 计算两个迭代器指向的元素之间的个数
* (node - x.node - 1)计算的是两个迭代器所在的线性空间中间跨过的完整的线性空间个数
*/
difference_type operator-(const self& x) const {
return difference_type(buffer_size()) * (node - x.node - 1) +
(cur - first) + (x.last - x.cur);
}
/* 重载操作符++(前)
* 这里可能涉及到跳到下一段线性空间的情况
* 先将cur指向下一个元素,cur如果等于last了,证明该段空间已经没有空间可以容纳下一个元素了
* 将node指向指针数组的下一个元素,即指向下一段线性空间,再将cur指向下段线性空间的首个元素
*/
self& operator++() {
++cur;
if (cur == last) {
set_node(node + 1);
cur = first;
}
return *this;
}
/* 重载操作符++(后) */
self operator++(int) {
self tmp = *this;
++*this;
return tmp;
}
/* 操作符--和++类似 */
self& operator--() {
if (cur == first) {
set_node(node - 1);
cur = last;
}
--cur;
return *this;
}
self operator--(int) {
self tmp = *this;
--*this;
return tmp;
}
/* 重载操作符+=
* 这里同样也涉及到可能当前线性空间容量不够
* 需要存储到下一段线性空间的情况
* 就不一一赘述了
*/
self& operator+=(difference_type n) {
difference_type offset = n + (cur - first);
if (offset >= 0 && offset < difference_type(buffer_size()))
cur += n;
else {
difference_type node_offset =
offset > 0 ? offset / difference_type(buffer_size())
: -difference_type((-offset - 1) / buffer_size()) - 1;
set_node(node + node_offset);
cur = first + (offset - node_offset * difference_type(buffer_size()));
}
return *this;
}
......
reference operator[](difference_type n) const { return *(*this + n); }
bool operator==(const self& x) const { return cur == x.cur; }
bool operator!=(const self& x) const { return !(*this == x); }
/* 比较两个迭代器的大小
* 首先判断两者是否处于同一段连续空间
* 若处于,则比较元素大小
* 若不处于,则比较node的大小
* 所以并不是只是单纯的比较元素的大小,这点需要特别注意
*/
bool operator<(const self& x) const {
return (node == x.node) ? (cur < x.cur) : (node < x.node);
}
/* 该函数用于设置node指向的线性空间
* 需要更新first和last指针
* 而cur指针由调用set_node的函数进行合适的修改
*/
void set_node(map_pointer new_node) {
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size());
}
};
以上便是deque
的迭代器部分,它的封装应该充分体现了STL中迭代器部分的精髓,屏蔽了内部的细节,将原本并不连续的空间,通过定义自己内部的指针以及重载大量的操作符,营造出迭代器的操作就跟指针一样的假象,可以使用++
,比大小等操作。
deque
内部常用的定义及声明
protected: // Data members
/* deque容器内部定义了两个迭代器指针
* start指向指针数组中正在使用的第一个结点
* finish指向指针数组中正在使用的最后一个结点
*/
iterator start;
iterator finish;
/* map即之前所说的指针数组
* 而指针数组中的每一个指针又指向一段连续的线性空间(简称为结点)
* map_size代表map中有多少个成员(指针)
map_pointer map;
size_type map_size;
public: // Basic accessors
iterator begin() { return start; }
iterator end() { return finish; }
const_iterator begin() const { return start; }
const_iterator end() const { return finish; }
reference operator[](size_type n) { return start[difference_type(n)]; }
const_reference operator[](size_type n) const {
return start[difference_type(n)];
}
reference front() { return *start; }
reference back() {
iterator tmp = finish;
--tmp;
return *tmp;
}
const_reference front() const { return *start; }
const_reference back() const {
const_iterator tmp = finish;
--tmp;
return *tmp;
}
size_type size() const { return finish - start;; }
size_type max_size() const { return size_type(-1); }
bool empty() const { return finish == start; }
以上便是一些基础的函数以及内部变量,这些都很简单,只用简单的看一下就行了。唯一需要关注的就是deque
内部使用了两个迭代器分别指向指针数组中指向开始的那段线性空间和结束的那段线性空间。
deque的内存申请及构造
deque
容器的构造函数同样也有多个版本,接下来我们来依次分析它们,通过这些构造函数以及析构函数,也可以了解到deque
关于内存的申请及释放。create_map_and_nodes
函数用于申请指针数组的空间以及用于存储元素的线性空间,这个我们放在构造函数之后讲,先知道它是干什么的。
默认构造函数
/* 指针数组不申请空间
* 并且first和finish迭代器也调用默认构造函数
*/
deque()
: start(), finish(), map(0), map_size(0)
{
create_map_and_nodes(0);
}
拷贝构造函数
/* 分配了空间之后
* 讲x上面的元素拷贝过去就行了
*/
deque(const deque& x)
: start(), finish(), map(0), map_size(0)
{
create_map_and_nodes(x.size());
__STL_TRY {
uninitialized_copy(x.begin(), x.end(), start);
}
__STL_UNWIND(destroy_map_and_nodes());
}
初始化n个值为value的元素
deque(size_type n, const value_type& value)
: start(), finish(), map(0), map_size(0)
{
fill_initialize(n, value);
}
deque(int n, const value_type& value)
: start(), finish(), map(0), map_size(0)
{
fill_initialize(n, value);
}
deque(long n, const value_type& value)
: start(), finish(), map(0), map_size(0)
{
fill_initialize(n, value);
}
初始化n个值为默认值的元素
explicit deque(size_type n)
: start(), finish(), map(0), map_size(0)
{
fill_initialize(n, value_type());
}
迭代器范围构造
template <class InputIterator>
deque(InputIterator first, InputIterator last)
: start(), finish(), map(0), map_size(0)
{
//range_initialize根据迭代器的类型选择最优的初始化方式
range_initialize(first, last, iterator_category(first));
}
create_map_and_nodes
该函数的作用如前所述,申请指针数组map的大小及线性空间。
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::create_map_and_nodes(size_type num_elements) {
/* 计算结点个数
* buffer_size()返回每段线性空间能容量元素的个数
* num_elements代表需要申请的元素的个数
* num_nodes代表需要申请的结点个数
*/
size_type num_nodes = num_elements / buffer_size() + 1;
/* map至少有8个结点,最多num_nodes + 2
* 之所以要设计成num_nodes + 2
* 是为了当插入元素分别到尾部和首部时有一定的扩展性,而不用一插入元素就分配空间
*/
map_size = max(initial_map_size(), num_nodes + 2);
//使用专门申请指针数组空间的空间配置器申请
map = map_allocator::allocate(map_size);
/* 将nstart和nfinish指向范围内的那一段结点尽量放在申请的总结点的中央
* 这样做是为了让首部和头部具有一样的可以容纳新元素的空间
*/
map_pointer nstart = map + (map_size - num_nodes) / 2;
map_pointer nfinish = nstart + num_nodes - 1;
map_pointer cur;
__STL_TRY {
/* 申请一整段线性空间
* allocate_node的原型如下
* return data_allocator::allocate(buffer_size();
*/
for (cur = nstart; cur <= nfinish; ++cur)
*cur = allocate_node();
}
# ifdef __STL_USE_EXCEPTIONS
catch(...) {
for (map_pointer n = nstart; n < cur; ++n)
deallocate_node(*n);
map_allocator::deallocate(map, map_size);
throw;
}
# endif /* __STL_USE_EXCEPTIONS */
//更新start和finish迭代器
start.set_node(nstart);
finish.set_node(nfinish);
start.cur = start.first;
finish.cur = finish.first + num_elements % buffer_size();
}
initialize函数
关于初始化元素的值,deque
提供了两种函数。一种是fill_initialize
,另一种是range_initialize
。
fill_initialize
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::fill_initialize(size_type n,
const value_type& value) {
//首先先申请空间
create_map_and_nodes(n);
map_pointer cur;
__STL_TRY {
//对每个结点依次初始化
for (cur = start.node; cur < finish.node; ++cur)
uninitialized_fill(*cur, *cur + buffer_size(), value);
/* 最后一个结点需要单独处理
* 因为可能并不会填满最后的整个线性空间
*/
uninitialized_fill(finish.first, finish.cur, value);
}
# ifdef __STL_USE_EXCEPTIONS
catch(...) {
//发生异常了,则销毁所有申请的空间
for (map_pointer n = start.node; n < cur; ++n)
destroy(*n, *n + buffer_size());
destroy_map_and_nodes();
throw;
}
# endif /* __STL_USE_EXCEPTIONS */
}
range_initialize
根据迭代器类型的不同,有两个版本。你可能会想,deque
容器的迭代器不就是random_access_iterator_tag
类型的吗,为什么这里还要区分。
这是因为范围构造的时候,可能这个迭代器是vector
的迭代器或者其他容器的迭代器,first
和last
迭代器并不是一定就是deque
容器的迭代器。
template <class T, class Alloc, size_t BufSize>
template <class InputIterator>
void deque<T, Alloc, BufSize>::range_initialize(InputIterator first,
InputIterator last,
input_iterator_tag) {
create_map_and_nodes(0);
//一个一个的插入
for ( ; first != last; ++first)
push_back(*first);
}
template <class T, class Alloc, size_t BufSize>
template <class ForwardIterator>
void deque<T, Alloc, BufSize>::range_initialize(ForwardIterator first,
ForwardIterator last,
forward_iterator_tag) {
size_type n = 0;
//计算出[first, last)范围内的元素个数
distance(first, last, n);
//申请空间
create_map_and_nodes(n);
//进行拷贝,若失败,销毁空间
__STL_TRY {
uninitialized_copy(first, last, start);
}
__STL_UNWIND(destroy_map_and_nodes());
}
deque的内存释放及析构
最后让我们再来看看一下deque
容器是如何释放内存以及析构函数的真面目。
前面异常处理的时候,基本都调用了destroy_map_and_nodes
函数。它的作用就时释放deque
容器的空间。
内存释放
源码如下:
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::destroy_map_and_nodes() {
//依次遍历每个结点,调用deallocate_node释放空间(这里是释放的指针数组中指针指向的线性空间)
for (map_pointer cur = start.node; cur <= finish.node; ++cur)
deallocate_node(*cur);
//释放指针数组
map_allocator::deallocate(map, map_size);
}
deallocate_node
函数很简单,源码就一行data_allocator::deallocate(n, buffer_size())
,调用了空间配置器里面的释放空间的函数。
析构函数
~deque() {
destroy(start, finish);
destroy_map_and_nodes();
}
小结
在本小节我们主要介绍了deque
容器的迭代器以及它使用的数据结构以及机制,还有关于内存的申请及构造函数以及内存的释放及析构函数。
重点是要理解deque
容器存储元素的机制,即使用指针数组,而每个指针指向的才是真正存储元素的线性空间。还有关于deque
的迭代器部分,也是比较关键的地方。在下一小节中,我们将调几个deque
容器比较常用的操作来分析,因为deque
的操作确实比较多….