容器是什么?
容器container
是用来存储数据的类模板, 封装了组织数据的各种方法。STL中有两类容器,序列式容器和关联式容器,如下图所示。
序列式容器主要包括vector、lis
t和deque
容器,其特点是元素在容器中的位置与元素值没有关系,即元素在容器中是无序的,元素插入到容器中,指定什么位置,元素就位于什么位置。
关联式容器主要包括set
、multiset
、map
和multimap
,其特点是容器内的元素会自动排序,元素插入的位置与元素在容器中的位置没有任何关系。
本文先介绍各种序列式容器的关键实现细节,关联式容器的工作原理将在下一篇文章中介绍。
list
list
容器,又称为双向链表容器,容器的底层以双向链表形式实现,其特点是list
中的元素可以分散存储在内存空间中,而不是必须存储在一块连续的内存空间中。
为什么首先讲list
容器,而不是vector
容器?原因是在上一篇迭代器文章中,我们初步实现了一个list
的iterator
,通过与STL
的实现可以更好的对比学习迭代器知识。
list节点
list
容器底层是以双向链表形式实现,因此底层的数据结构list node
的定义如下:
// list node
template <class T>
struct __list_node
{
typedef voide* void_pointer;
void_pointer prev;
void_pointer next;
T data;
};
list专属迭代器
通过上一篇文章迭代器解析,我们知道迭代器是一个smart poiner
,即有能力进行正确的递增、递减、取值、成员访问等操作;为了能使iterator traits
能有效工作,迭代器需要以内嵌型别定义nested typedef
方式定义五种常见的型别信息。因此,迭代器设计分为如下两块:
smart pointer
typedef __list_node<T>* link_type;
link_type node; // 迭代器内部指针, 指向list节点
// 实现递增、递减、取值和成员访问等运算符重载
- 五种型别信息
typedef T value_type;
typedef Ref reference;
typedef Ptr pointer;
typedef ptrdiff_t difference_type;
typedef bidirectional_iterator_tag iterator_category;
list
迭代器的设计如下:
// list node的专属迭代器
template <class T, class Ref, class Ptr>
struct __list_iterator
{
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, Ref, Ptr> self;
typedef __list_node<T>* link_type;
typedef size_t size_type;
typedef T value_type;
typedef Ref reference;
typedef Ptr pointer;
typedef ptrdiff_t difference_type;
typedef bidirectional_iterator_tag iterator_category;
link_type node;
// constructor
__list_iterator(link_type x) : node(x) {
}
__list_iterator() {
}
__list_iterator(const iterator& x) : node(x.node) {
}
bool operator== (const self& x) const {
return node == x.node;}
bool operator!= (const self& x) const {
return node != x.node;}
// 对迭代器取值(derefence), 取的是节点的数据值
reference operator* () const {
return (*node).data;}
// 对迭代器的成员进行访问
pointer operator-> () const {
return &(operator*());}
// 对迭代器累加1, 前进一步
self& operator++ ()
{
node = (link_type)((*node).next);
return *this;
}
self operator++(int)
{
self tmp = *this; // 调用拷贝构造函数
++*this;
return tmp; // 调用拷贝构造函数
}
// 对迭代器递减1, 就是后退一个节点
self& operator--()
{
node = (link_type)((*node).prev);
return *this;
}
self operator--(int)
{
self tmp = *this;
--*this;
return tmp;
}
};
在list迭代器中,递增时指向下一个节点,递减时指向上一个节点,取值时取得是节点的data值,如下图所示:
其中后置递增操作的工作过程如下,首先调用拷贝构造函数,记录原值,然后对迭代器本身进行加一操作,最后返回记录的原值。
operator->
的工作原理在上一篇迭代器文章中已经介绍过了,简单来说,但operator->
的返回值为指针时,右操作数为指针执行数据类型的成员。
list的内存管理
list中缺省使用alloc作为空间配置器,并据此定义了一个list_node_allocator,以便以节点大小分配内存。
template <class T, class Alloc = alloc>
class list
{
protected:
typedef __list_node<T> list_node;
// 空间配置器, 每次配置一个节点大小
typedef simple_alloc<list_node, Alloc> list_node_allocator;
...
}
其中配置、释放、构造、销毁一个节点的实现如下:
// 给一个list节点分配内存, 返回一个list节点指针
link_type get_node() {
return list_node_allocator::allocate();}
// 释放一个list节点内存
void put_node(link_type p) {
list_node_allocator::deallocate(p);}
// 产生一个节点, 包括节点内存分配和调用构造函数
link_type create_node(const T& x)
{
link_type p = get_node();
construct(&p->data, x); // 全局函数, 构造对象
return p;
}
// 销毁(析构并释放)一个节点
void destroy_node(link_type p)
{
destroy(&p->data); // 全局函数, 析构对象
put_node(p);
}
list的数据结构
SGI list
不仅是一个双向链表,而且还是一个环状双向链表。只需要一个指针,便可以完整地表示整个链表,因此,list
中只有一个指针成员变量。
// 对list node数据进行处理
template <class T, class Alloc = alloc>
class list
{
protected:
typedef __list_node<T> list_node;
// 空间配置器, 每次配置一个节点大小
typedef simple_alloc<list_node, Alloc> list_node_allocator;
public:
typedef list_node* link_type;
protected:
link_type node; // 只要一个指针, 便可表示整个环状双向链表
...
}
STL 中通过将成员指针变量node指向一个空白节点(哨兵节点),来符合“前闭后开”的区间要求。其中空白节点,在每个构造函数中都会通过调用empty_initialize函数构建。以默认构造函数为例,代码如下:
public:
// default constructor
list() {
empty_initialize();} // 产生一个空链表
protected:
void empty_intialize()
{
node = get_node(); // 配置一个节点空间, 令成员node指向它
node->next = node; // 令node的头尾都指向自己,不设元素值
node->prev = node;
}
空白节点的结构如下:
list示意图如下:
当将list的成员变量node指针特意指向空白指针之后,可以轻松的完成一下几个函数:
iterator begin() {
return (link_type)((*node).next); }
iteator end() {
return node; }
bool empty() const {
return node->next == node;}
size_type size() const
{
size_type result = 0;
distance(begin(), end(), result); // 全局函数
return result;
}
// 取头节点的元素值
reference front() {
return *begin();}
// 取为节点的元素值
reference back() {
return *(--end());}
在list中有三个很重要的成员函数:insert、erase和transfer,很多成员函数都是通过调用这三个函数实现。
insert函数
insert
是一个重载函数,有多种形式,其功能是在给定迭代器所指位置的前方插入一个节点。需要重点注意新的节点插入的位置是给定迭代器所指节点的前方。insert
的代码实现如下:
// insert函数: 在迭代器position所指位置的前方插入一个节点, 内容为x
iterator insert(iteartor postion, const T& x)
{
link_type tmp = create_node(x);
// 调整双向指针, 将tmp插入进去
tmp->next = position.node; // 1
tmp->prev = position.node->prev; // 2
(link_type(position.node->prev))->next = tmp; // 3
position.node->prev = tmp; // 4
return tmp; // iterator(tmp)
}
插入一个节点,指针变换过程如下图所示,下图指的是输入迭代器为end(),插入一个节点,可以看到end()所指向节点的prev指针指向的就是这个新的节点,也即插入节点在给定迭代器所指节点的前方。
以下函数内部通过调用insert函数完成操作。
// ************** 调用insert函数 ************** //
// 插入一个节点, 作为头节点
void push_front(const T& x) {
insert(begin(), x);}
// 插入一个节点, 作为尾节点
void push_back(const T& x) {
insert(end(), x);}
// ****************************************** //
erase函数
erase
函数的功能是移除给定迭代器所指向的节点。
// erase函数: 移除迭代器position所指位置的节点, 并返回下一个节点
iterator erase(iterator position)
{
link_type next_node = (link_type)(positon.node->next); // 1
link_type prev_node = (link_type)(position.node->prev); // 2
prev_node->next = next_node; // 3
next_node->prev = prev_node; // 4
destroy_node(position.node); // 5
return iterator(next_node);
}
删除list
中一个节点的操作流程入下图所示:
以下函数内部通过调用erase
函数完成操作。
// ************* 调用erase函数 ************* //
// 移除节点
void pop_front() {
erase(begin());}
// 移除尾节点
void pop_back()
{
iterator tmp = end();
erase(--tmp);
}
// 清除所有节点
void clear()
{
link_type cur = (link_type) node->next; // begin()
while (cur != node) // begin() != end()
{
link_type tmp = cur;
cur = (link_type) cur->next;
destroy_node(tmp) ; // 销毁一个节点
}
// 恢复node原始状态
node->next = node;
node->prev = node;
}
// 将数值为value的所有元素移除
void remove(const T& value)
{
iterator first = begin();
iterator last = end();
while(first != last)
{
iterator next = first;
++next;
if(*first == value) erase(first); // 找到就移除
first = next;
}
}
// 移除数值相同的连续元素
// “连续并且相同的元素”才会被移除剩一个
void unique()
{
iterator first = begin();
iterator last = end();
if(first == last) return; // 空链表, 什么都不必做
iterator next = first;
while(++next != last)
{
if(*first == *next) // 如果在此区段中有相同的元素
erase(next);
else
first = next;
next = first;
}
}
// ************************************* //
};
transfer函数
transfer
函数是list
内部提供的一个迁移操作,其功能是将某连续范围的元素迁移到某个特定位置之前。transfer
的源代码如下:
// 将[first, last)内所有元素移动到position之前
void transfer(iterator position, iterator first, iterator last)
{
if(position != last)
{
(*(link_type((*last.node).prev))).next = position.node; // 1
(*(link_type((*first.node).prev))).next = last.node; // 2
(*(link_type((*position.node).prev))).next = first.node; // 3
link_type tmp = link_type((*position.node).prev); // 4
(*position.node).prev = (*last.node).prev; // 5
(*last.node).prev = (*first.node).prev; // 6
(*first.node).prev = tmp; // 7
}
}
总共包含7个步骤,如下图所示:
transfer
函数并不是stl
的公开接口,而是splice
函数,以下是splice
函数的各种版本实现:
// splice函数
// 将另一个list结合在position所指位置之前
void splice(iterator position, list& x)
{
if(!x.empty())
transfer(position, x.begin(), x.end());
}
// 将i所指节点接合于position所指节点之前, position和i可以指向同一个list
void splice(iterator position, list&, iterator i)
{
iterator j = i;
++j;
if(position==i || position==j) return;
transfer(position, i, j);
}
// 将[first, last)内所有元素结合于position所指节点之前
// position和[first, last)可指向同一个list
// 但是position不能位于[first, last)之内
void splice(iterator position, list&, iterator first, iterator last)
{
if(first != last)
transfer(position, first, last);
}
此外,sort
、merge
和reverse
函数都是通过在内部调用transfer
函数完成的,在这里就不做赘述。
vector
vector
的数据安排及操作方式,与array
非常相似,唯一的区别在于,array
是静态空间,空间大小一旦配置就不能再改变,而vector是动态空间,随着元素加入,其内部会自动扩充空间以容纳新元素,下图非常形象的展示它们之间的区别。
vector元素
vector
是template class
,其内部存储的元素类型在创建对象时指定,如vector<int>
、vector<string>
等,vector
容器中存储的元素类型分别是int
或者string
。vector
容器中只能存储同一种类型的元素。
vector迭代器
vector的专属迭代器,其实用vector元素对应数据类型的指针即可,为什么这么说呢?我们先可以仿照list的迭代器编写vector迭代器,代码如下:
template <class T, class Ref, class Ptr>
struct __vector_iterator
{
typedef __vector_iterator<T, T&, T*> iterator;
typedef __vector_iterator<T, Ref, Ptr> self;
typedef T* link_type;
typedef size_t size_type;
typedef T value_type;
typedef Ref reference;
typedef Ptr pointer;
typedef ptrdiff_t difference_type;
typedef random_access_iterator_tag iterator_category;
link_type elem_ptr; // 需要一个指针, 指向vector容器
// constructor
__list_iterator(link_type x) : elem_ptr(x) {
}
__list_iterator() {
}
__list_iterator(const iterator& x) : elem_ptr(x) {
}
bool operator== (const self& x) const {
return elem_ptr == x.elem_ptr;}
bool operator!= (const self& x) const {
return elem_ptr != x.elem_ptr;}
// 对迭代器取值(derefence), 取的是节点的数据值
reference operator* () const {
return *elem_ptr;}
// 对迭代器的成员进行访问
pointer operator-> () const {
return elem_ptr;}
// 对迭代器累加1, 前进一步
self& operator++ ()
{
++this;
return *this;
}
self operator++(int)
{
self tmp = *this;
++*this;
return tmp;
}
// 对迭代器递减1, 就是后退一个节点
self& operator--()
{
--this;
return *this;
}
self operator--(int)
{
self tmp = *this;
--*this;
return tmp;
}
};
通过之前的学习,可以知道设计迭代器有两个重点:
- 迭代器是一个smart pointer,需要完成递增、递减、取值、成员访问等操作,但是观察上面各种operator的实现可以看出只是对指针的operator的简单封装;
- 迭代器需要以内嵌型别定义
nested typedef
方式定义五种常见的型别信息,但是从上一篇文章中知道iterator traits
对普通指针和常量指针都做了偏特化处理,因此,当传入一个指针时,经过iterator traits
能自动得到五种常见的型别信息;
综上,在vector
容器中,使用vector
元素数据类型对应的普通指针作为迭代器即可满足所有要求。
vector的内存管理
vector
缺省使用alloc
作为空间配置器,并据此另外定义了一个data_allocator
,使其能够一次分配N个元素大小空间:
template <class T, class Alloc = alloc>
class vector
{
protected:
typedef T value_type;
typedef value_type* iterator; // 使用普通指针即可满足迭代器的需求
typedef simple_alloc<value_type, Alloc> data_allocator;
...
};
因此,通过data_allocator::allocate(n)
即可配置n
个元素空间。
vector的数据结构
template <class T, class Alloc = alloc>
class vector
{
public:
typedef T value_type;
typedef value_type* iterator; // 使用普通指针即可满足迭代器的需求
typedef value_type* pointer;
typedef valure_type& reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
protected:
typedef simple_alloc<value_type, Alloc> data_allocator; // 空间配置器
// 成员变量
iterator start; // 表示目前使用空间的头
iterator finish; // 表示目前使用空间的尾
iterator end_of_storage; //表示目前可用空间的尾
...
};
vector
类中只有三个迭代器(普通指针),start
和finish
分别指向当前配置得到的连续空间中已被使用的范围,end_of_storage
则指向已经配置连续空间的尾端。
通过上述三个迭代器,可以很轻松完成如下方法的功能。其中capacity指的是连续空间配置的大小,size指的是当前连续空间已被使用的大小。
public:
iterator begin() {
return start;}
iterator end() {
return finish;}
size_type size() const {
return size_type(end() - begin());}
size_type capacity() const {
return size_type(end_of_storage - begin());}
bool empty() const {
return begin() == end();}
reference operator[](size_type n) {
return *begin() + n;}
reference front() {
return *begin();}
reference back() {
return *(end() - 1);}
...
vector的构造函数
如上图所示,vector
中提供了4种构造函数,其中fill
指的是使用同一个值初始n个元素空间,range
指的是使用已有容器中一段区间内的数据初始化vector
容器,copy
指的是使用已有的容器初始化新的vector
容器。后面3种构造函数的代码如下,内部通过调用unintialized_fill_n
和uninitialized_copy
全局函数完成。
// 构造函数
vector(size_type n, const T& value) {
fill_initialize(n, value);}
void fill_initialize(size_type n, const T& value)
{
start = allocate_and_fill(n, value);
finish = start + n;
end_of_storage = finish;
}
iterator allocate_and_fill(size_type, n, const T& value)
{
iterator result = data_allocator::allocate(n); // 配置n个元素空间
unintialized_fill_n(result, n, x) ; // 全局函数
return result;
}
vector (InputIterator first, InputIterator last,
const allocator_type& alloc = allocator_type())
{
finish = uninitialized_copy(first, last, start); // 全局函数
}
vector(const vector<T, alloc>& x)
{
finish = uninitialized_copy(x.begin(), x.end(), start); // 全局函数
}
insert函数
vector
容器中,通过底层调用insert_aux
函数来插入数据,并自动动态增加大小,具体做法是以原容器大小的两倍另外配置一块空间,然后将原内容拷贝过来,然后开始在原内容之后构造元素,并释放原空间。具体代码和流程图如下:
void insert_aux(iterator position, const T& x)
{
if(finish != end_of_storage) // 还存在备用空间
{
// 在备用空间起始处构造一个元素,并以vector最后一个元素值作为其初值
construct(finish, *(finish - 1));
++finish;
T x_copy = x;
copy_backward(position, finish - 2, finish - 1);
*position = x_copy;
}
else // 没有备用空间
{
const size_type old_size = size();
const size_type len = old_size != 0 ? 2 * old_size : 1;
iterator new_start = data_allocator::allocate(len); // 实际配置空间
iterator new_finish = new_start;
try
{
// 将原vector的内容拷贝到新的vector
new_finish = unintialized_copy(start, position, new_start);
// 为新元素设定初值x
construct(new_finish, x);
++new_finish;
new_finish = unintialized_copy(position, finish, new_finish);
}
catch(...)
{
...
}
// 析构并释放原vector
destroy(begin(), end());
deallocate();
// 调整迭代器, 指向新的vector
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
由于空间重新配置,原vector的所有迭代器都失效,不能获取到正确的数据。下面是一个小例子。
// inserting into a vector
#include <iostream>
#include <vector>
int main ()
{
std::vector<int> myvector (3,100); // 分配3个元素空间, 调用unintialized_fill_n全局函数初始化元素内容为100
std::vector<int>::iterator it;
it = myvector.begin(); // 获取得到当前vector容器的start迭代器
myvector.insert ( it , 200 ); // 想vector容器插入一个值为200的数据, 但是, 此时vector容器中已经没有可用空间,会重新分配两倍内存, 6个字节
// it此时不再指向vector的begin
std::cout << "myvector contains:";
for (it=myvector.begin(); it<myvector.end(); it++) // 需要重新获取
std::cout << ' ' << *it;
std::cout << '\n';
return 0;
}
push_back
函数将新元素插入到vector
尾端时,会首先判断是否还存在备用空间,如果有则直接构建元素,并调整迭代器。如果没有则通过调用insert_aux
函数扩充空间。
erase函数
erase
函数支持删除(析构)[first, last)
中所有元素和指定某个位置元素两种形式,下面主要讲解如何清除[first, last)
中所有元素的代码和工作流程,如下所示:
iterator erase(iterator first, iterator last)
{
iterator it = copy(last, finish, first);//1
destory(i, finish); // 2
finish = finish - (last - first); // 3
return first;
}
clear
函数通过直接调用上面的erase
函数来清除vector
中所有的元素,代码如下:
void clear () {
erase(begin(), end()); }
需要强调的一点是,erase
函数只是将元素析构,但是并没有释放对应的内存空间。
deque
与vector单向开口不同,deque是一种双向开口的连续线性空间,可以在头尾两端分别做元素的插入和删除操作,如图所示。
deque的中控器
deque
的空间是由一段段连续空间组合而成,通过动态的增加一段新的空间来增加内存空间。为了管理这一段段连续空间,deque
采用一块所谓的map
,不是STL
的map
容器,作为主控。map
是一小块连续空间,其中每个元素都是一个指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque
的存储空间主体。
map在deque中的定义代码如下:
template <class T, class Alloc=alloc, size_t BufSize = 0>
class deque
{
public:
typedef T value_type;
typedef value_type* pointer;
...
protected:
// 元素的指针的指针
typedef pointer* map_pointer;
protected:
map_pointer map;
size_type map_size; // map内可容纳多少指针
};
SGI STL
允许指定缓冲区大小,默认值为0表示使用512 bytes
缓冲区。map
的类型是T**
,元素是一个指针,所指之物是型别为T
的一块空间。
deque的迭代器
deque
采用的是分段连续空间,而表面维持“整体连续”,是通过迭代器的operator--
和operator++
两个操作实现。
- 以内嵌型别定义
nested typedef
方式定义五种常见的型别信息
template <class T, class Ref, class Ptr, size_t BufSize>
struct __deque_iterator{
// 未继承std::iterator
// 未继承std::iterator,所以自行撰写五个必要的迭代器相应类别
typedef random_access_iterator_tag iterator_category; // 1
typedef _Tp value_type; // 2
typedef _Ptr pointer; // 3
typedef _Ref reference; // 4
typedef ptrdiff_t difference_type; // 5
...
};
- 迭代器本质是一个
smart pointer
,由于需要能够指出分段连续扣减在哪里, 还需要判断自己是否处于其所在缓冲区的边缘,deque
迭代器中包含了三个型别指针对象cur
,first
,last
。此外还需要能准确地在上一个或下一个缓冲区之间跳跃,因此,包含了管控中map
。
typedef T** map_pointer;
// 保持与容器的联结
T* cur;
T* first;
T* last;
map_pointer node;
...
deque
的中控器、缓冲区、迭代器的相互关系如下:
其中,迭代器中还提供了一个决定缓冲区大小的函数buffer_size()
,返回的结果指的是一个缓冲区能存放多少个元素,内部通过调用__deque_buf_size()
全局函数完成,代码如下:
static size_t _S_buffer_size() {
return __deque_buf_size(sizeof(_Tp)); }
// 如果 n 不为 0,传回 n,表示 buffer size由使用者自定。
// 如果 n 为 0,表示 buffer size使用默认值,那么
// 如果sz(元素大小,sizeof(value_type))小于 512,传回 512/sz,
// 如果sz不小于 512,传回 1。
inline size_t __deque_buf_size(size_t __size) {
return __size < 512 ? size_t(512 / __size) : size_t(1);
}
运算符重载
deque的迭代器需要在不同缓冲区之间进行跳转,因此,对各种指针运算进行重载操作十分必要。
- 跳转一个缓冲区,调整迭代器对应的
first
和last
指针
void set_node (map_pointer new_node)
{
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size());
}
dereference
和成员访问
reference operator*() const {
return *cur; }
pointer operator->() const {
return &(operator*()); }
- 减号运算符
difference_type operator-(const self& x) const
{
return difference_type(buffer_size()) * (node - x.node - 1) +
(cur - first) + (x.last - x.cur);
}
- 自增和自减
self& operator++()
{
++cur; // 切换至下一个元素
if(cur == last ) // 如果已经到达缓冲区的尾端
{
set_node(node + 1); // 就切换至下一节点
cur = first; // 第一个元素
}
return *this;
}
slef& operator--()
{
if(cur == first) {
// 如果已经到达缓冲区的头端
set_node(node - 1);
cur = last;
}
--cur;
return *this;
}
- 随机存取
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;
}
self& operator-=(difference_type n) {
return *this += -n; }
// 实现随机存取
reference operator[] (difference_type n) const {
return *(*this + n);}
deque的数据结构
deque中的成员除了一个指向map的指针之外,还需要维护两个迭代器start,finish,分别指向第一个缓冲区的第一个元素和最后缓冲区的最后一个元素。此外还需要记住目前map的大小,当map提供的节点不足时,必须重新配置更大的一块map。
template <class T, class Alloc=alloc, size_t BufSize = 0>
class deque
{
public:
typedef T value_type;
typedef value_type* pointer;
...
protected:
// 元素的指针的指针
typedef pointer* map_pointer;
protected:
map_pointer map;
size_type map_size; // map内可容纳多少指针
iterator start ;
iterator finish;
...
};
能很轻易得到下面这些函数的实现:
public:
iterator begin() {
return start;}
iterator end() {
return finish;}
reference operator[](size_type n){
return start[difference_type(n)];}
reference front() {
return *start; }
reference back() {
iterator tmp = finish;
--tmp;
return *tmp;
}
size_type size() cosnt {
return finish - start;}
size_type max_size() const {
return size_type(-1); }
bool empty() const {
return finish == start; }
deque的构造与内存管理
如下代码所示,设置缓冲区大小为8,即可以保存8个元素,并令其保留20个元素空间,每个元素初值为9。
deque<int, alloc, 8> ideq(20, 9);
上述deque
对象配置完内存和初始化之后,整体的示意图如下:
deque
自行定义了两个专属的空间配置器:
protected:
//专属之空间配置器, 每次配置一个元素大小
typedef simple_alloc<value_type, Alloc> data_allocator;
// 专属之空间配置器, 每次配置一个指针大小
typedef simple_alloc<pointer, Alloc> map_allocator;
deque
的一个constructor
的定义如下:
deque(int n, const value_type& value)
: start (), finish (), map(0), map_size(0)
{
fill_initialize(n, value);
}
内部所调用的fill_initialize()
负责产生并安排好deque
的结构,并将元素的初值设定妥当。
添加元素
通过在尾端继续插入三个新元素,此时,由于最后一个缓冲区有4个备用元素空间,所有不会引起缓冲区的再配置,结构如下图所示:
for(int i=0; i<3; i++)
ideq.push_back(i);
push_back()函数内容如下:
public:
void push_back(const value_type& t)
{
if(finish.cur != finish.last - 1)
{
// 最后缓冲区尚存在两个以上的元素备用空间
construct(finish.cur, t); // 直接在备用空间构造元素
++finish.cur; // 调整最后缓冲区的使用状态
}
else // 最后缓冲区只剩下一个元素备用空间
push_back_aux(t);
}
之后,再在尾端添加一个新元素,此时由于最后的缓冲区只有一个元素备用空间,则会调用push_back_aux
,先配置一整块新的缓冲区,再设置新元素内容,然后更改迭代器finish
的状态,新的结构如下:
同样,通过调用push_front()
在前端插入一个元素,首先判断第一缓冲区是否存在备用空间,如果没有则调用push_front_aux()
函数再配置一块新缓冲区,设定新元素,修改迭代start
的状态,新的结构如下图所示:
调整map
deque中,什么时候需要调整map,由reserve_map_at_back()和reserve_map_at_front()进行判断,实际操作则是由reallocate_map()执行。
// 如果 map 尾端的节点备用空间不足,符合条件就配置一个新的map(配置更大的,拷贝原来的,释放原来的)
void reserve_map_at_back (size_type nodes_to_add = 1) {
if (nodes_to_add + 1 > map_size - (finish.node - map))
reallocate_map(nodes_to_add, false);
}
// 如果 map 前端的节点备用空间不足,符合条件就配置一个新的map(配置更大的,拷贝原来的,释放原来的)
void reserve_map_at_front (size_type nodes_to_add = 1) {
if (nodes_to_add > start.node - map)
reallocate_map(nodes_to_add, true);
}
删除元素
deque
中,使用pop_front()
和pop_back()
可以从首端或者尾端删除元素。当一个缓冲区中没有元素时,还需要缓冲区释放掉,代码如下:
void pop_back()
{
if(finish.cur != finish.first){
--finish.cur; // 调整指针,相当于删除了最后元素
destroy(finish.cur); // 析构最后的元素
}
else
pop_back_aux();
}
void pop_back_aux() {
deallocate_node(finish.first) ; // 是否最后一个缓冲区
finish.set_node(finish.node - 1); //调整迭代器finish的状态
finish.cur = finish.last - 1;
destroy(finish.cur) ; // 将该元素析构
}
clear()
函数,用来清除整个deque
。需要注意的是,clear()
完成之后,deque
还原成最初状态只有一个缓冲区。
void clear()
{
// 对头尾以外的每个缓冲区的元素都析构,且释放缓冲区的内存
for(map_pointer node=start.node + 1; node < finish.node; ++node)
{
destroy(*node, *node + buffer_size();
// 释放缓冲区内存
data_allocator::deallocate(*node, buffer_size());
}
if(start.node != finish.node){
// 至少有头尾两个缓冲区
destroy(start.cur, start.last);
destroy(finish.first, finish.cur);
// 释放尾端缓冲区
data_allocator::deallocate(finish.first, buffer_size());
}
else
destroy(start.cur, finish.cur);
finish = start; // 调整状态
}
erase()
函数为了提高效率,会根据删除位置距离start
近还是finish
近来决定移动。
public: // erase
iterator erase(iterator pos) {
iterator next = pos;
++next;
difference_type index = pos - start;
// 删除的地方是中间偏前, 移动前面的元素
if (index < (size() >> 1)) {
copy_backward(start, pos, next);
pop_front();
}
// 删除的地方是中间偏后, 移动后面的元素
else {
copy(next, finish, pos);
pop_back();
}
return start + index;
}
// 范围删除, 实际也是调用上面的erase函数.
iterator erase(iterator first, iterator last);
同样的,insert()
函数也会根据插入位置距离首尾的大小,来移动不同的数据内容,提高效率。