STL源码剖析(十)序列式容器之deque
文章目录
前面我们说的vector是单向开口,而deque是双向开口,也就是保证常数时间在头部和尾部插入元素
一、deque的数据结构
我们先讨论deque是如何实现双向开口的,也就是可以双向增长?
deque是动态地通过分段连续的空间组成的,也就是说,deque可以随时分配一块内存,连接到原空间的前部或者后部。deque扩容并不会像vector一样,申请一段更大的空间,然后将数据拷贝过去,再释放原内存
deque为了管理这些分段连续的内存空间,使用了一个中控器,中控器是实际是一个指针数组,被使用的元素指向一段连续空间,如下所示
下面我们来看看deque的数据结构
template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
typedef T value_type;
typedef value_type* pointer;
typedef pointer* map_pointer;
...
typedef __deque_iterator<T, T&, T*> iterator;
...
protected: // Data members
/* 迭代器 */
iterator start;
iterator finish;
/* 中控器 */
map_pointer map;
size_type map_size;
...
};
可以看到,deque中有一个map
和map_size
,它们维护着deque的中控器。map
其实是一个指针数组,其数组项为value_type*
类型,指向一块连续的缓存区,map_size
表示该数组项元素个数
另外还有两个迭代器start
和finish
,start
指向首元素,finish
指向最后一个元素的下一个元素
deque提供random_access_iterator_tag
类型的迭代器,而deque内部并不是连续的内存,它只是对外连续,内部是分段连续,所以deque的迭代器为了实现这样的功能,较为复杂
deque内部的数据结构可以用以下图表示
还有一个问题就是,deque的每个缓存区有多大呢?
可以通过类模板中的BufSiz
指定,默认情况下是512个字节
二、deque的迭代器
下面再来分析deque的迭代器,它实现的是一个random_access_iterator_tag
类型的迭代器,而deque内部并不是连续的,所以它的实现较为复杂
它的定义如下
template <class T, class Ref, class Ptr>
struct __deque_iterator {
/* 满足STL迭代器的设计规范 */
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;
/* 指向指针的指针,指向中控器的一个节点 */
typedef T** map_pointer;
T* cur; //缓存区当前元素
T* first; //缓存区开头
T* last; //缓存区结尾
map_pointer node; //指向中控器的一个节点
...
};
deque的迭代器中有四个变量,cur
指向缓存区的当前元素,first
指向缓存区开头,last
指向缓存区结尾,node
指向中控器的某个节点,如下图所示
所以begin和finish迭代器如下图所示
看完迭代器的数据结构,再来看看它的操作
operator++
self& operator++() {
++cur; //cur指针指向下一个
if (cur == last) { //如果到缓存区结尾
set_node(node + 1); //跳到下一个缓存器
cur = first; //缓存区的第一个节点
}
return *this; //返回操作后的迭代器
}
首先cur指针指向下一个元素,如果到达缓存区的结尾,那么就跳到下一个缓存区,再指向该缓存区的第一个元素
set_node
用于跳转缓存区,我们之后再讨论
operator–
self& operator--() {
if (cur == first) { //如果cur指针在缓存区的第一个元素
set_node(node - 1); //跳到上一个缓存区
cur = last; //cur指向缓存区结尾
}
--cur; //cur向前移动一格
return *this; //返回操作后的迭代器
}
如果cur在缓存区的起始,显然该缓存已经无法再往前移动了,所以需要跳到上一个缓存区,然后再往前移动一格
set_node
下面再看看set_node是怎么跳转缓存区的
可以通过set_node(node + 1)
跳到下一个缓存区,set_node(node - 1)
跳到上一个缓存区
为什么node + 1
是下一个缓存区,node - 1
是上一个缓存区?
别忘了,中控器是一个指针数组,而迭代器中的node
成员指向其中的某个元素,所以自然可以通过±来跳转缓存区
下面再看看set_node
的实现
void set_node(map_pointer new_node) {
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size()); //节点个数
}
首先设置迭代器node
指向新节点,然后设置迭代器的first
指向缓存区起始,last
指向缓存区的结尾
上面代码通过first + difference_type(buffer_size())
获取缓存区结尾,显然buffer_size()
可以获取一个缓存区可容纳的最大元素个数,其定义如下
static size_t buffer_size() {return __deque_buf_size(0, sizeof(T)); }
inline size_t __deque_buf_size(size_t n, size_t sz)
{
return n != 0 ? n : (sz < 512 ? size_t(512 / sz) : size_t(1));
}
上面说过dequue每个缓存区的默认大小为512个字节
上述函数,当n=0的时候,表示缓存区使用默认大小512个字节
如果一个元素的大小小于512字节,那么就返回size_t(512 / sz)
否则返回1
三、deque的操作
3.1 构造函数
默认构造函数
deque()
: start(), finish(), map(0), map_size(0)
{
create_map_and_nodes(0);
}
通过调用create_map_and_nodes
来建立中控器
下面看一个create_map_and_nodes
的实现
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::create_map_and_nodes(size_type num_elements) {
/* num_elements表示元素个数,buffer_size表示一个缓存区能容纳多少个元素 */
size_type num_nodes = num_elements / buffer_size() + 1; //需要几个缓存区
map_size = max(initial_map_size(), num_nodes + 2); //中控器的节点数
map = map_allocator::allocate(map_size); //为中控器申请内存
/* 将中控器的起始节点和结束节点设置到中间位置,以便两端可以同等扩展 */
map_pointer nstart = map + (map_size - num_nodes) / 2; //中控器的起始节点
map_pointer nfinish = nstart + num_nodes - 1; //中控器的结束节点
/* 为现在使用的中控器节点分配缓存区 */
map_pointer cur;
for (cur = nstart; cur <= nfinish; ++cur)
*cur = allocate_node();
/* 设置起始迭代器和结尾迭代器 */
start.set_node(nstart); //设置起始迭代器对应中控器的节点
finish.set_node(nfinish); //设置结尾迭代器对应中控器的节点
start.cur = start.first; //起始迭代器位置
finish.cur = finish.first + num_elements % buffer_size(); //结尾迭代器指向
}
函数传入num_elements
,表示元素个数,buffer_size()
表示一个缓存区可以容纳多少个元素,通过num_elements / buffer_size() + 1
算的需要多少个缓存区
然后通过map_size = max(initial_map_size(), num_nodes + 2)
取得中控器的节点个数,max表示取最大值initial_map_size()
的结果为8
之后再为中控器申请内存map = map_allocator::allocate(map_size)
然后居中找到中控器的起始节点nstart
和结尾节点nfinish
,再为每个节点分配缓存区
然后再设置好deque容器start
迭代器,使其指向第一个元素位置,设置好finish
的迭代器,使其指向最后一个元素的下一个位置
拷贝构造
deque(const deque& x)
: start(), finish(), map(0), map_size(0)
{
create_map_and_nodes(x.size()); //创建中控器
uninitialized_copy(x.begin(), x.end(), start); //复制数据
}
首先通过create_map_and_nodes
创建中控器分配缓存区,然后再通过uninitialized_copy
将所有元素拷贝过去
3.2 析构函数
~deque() {
destroy(start, finish);
destroy_map_and_nodes();
}
首先通过destroy
析构所有的元素,然后通过destroy_map_and_nodes
来释放缓存区还有中控器
destroy_map_and_nodes
的定义如下
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::destroy_map_and_nodes() {
for (map_pointer cur = start.node; cur <= finish.node; ++cur)
deallocate_node(*cur);
map_allocator::deallocate(map, map_size);
}
上述函数,通过deallocate_node
来释放缓存区,然后通过空间配置器map_allocator
来释放中控器
其中deallocate_node
定义如下
void deallocate_node(pointer n) {
data_allocator::deallocate(n, buffer_size());
}
它是通过空间配置器data_allocator
来释放内存
3.3 添加元素
deque添加元素的方法有两种,一种是通过push
,一种通过insert
,其中push
分为两种,在尾部插入push_back
,在头部插入push_front
push_back
push_back是在结尾添加元素,其定义如下
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
处理
下面是push_back_aux
的定义
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_back_aux(const value_type& t) {
value_type t_copy = t;
reserve_map_at_back(); //中控器的尾部储备好足够的空闲节点
*(finish.node + 1) = allocate_node(); //为中控器节点分配缓存区
construct(finish.cur, t_copy); //构造对象
finish.set_node(finish.node + 1); //移动尾部迭代器指向的缓存区
finish.cur = finish.first; //设置好尾部迭代器
}
首先通过reserve_map_at_back
确保尾部储备了足够的空闲节点,就像上面画的图,中控器并不是所有的节点都指向缓存区,而是有一部分空闲的节点,供给扩展内存时使用
在确保中控器尾部有足够的空闲节点后,就在尾部新增一个缓存区
然后在原缓存区的最后一个位置,构造对象,然后重新设置结尾迭代器finish
下面看看reserve_map_at_back
如何确保中控器结尾有足够的节点,其定义如下
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);
}
如果中控器尾部的空闲节点数不足1,那么就调用reallocate_map
重新分配中控器,其定义如下
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::reallocate_map(size_type nodes_to_add,
bool add_at_front) {
size_type old_num_nodes = finish.node - start.node + 1; //旧的已用节点数
size_type new_num_nodes = old_num_nodes + nodes_to_add; //新需求节点数
map_pointer new_nstart;
if (map_size > 2 * new_num_nodes) { //如果中控器已有的节点数大于新需求节点数的两倍
new_nstart = map + (map_size - new_num_nodes) / 2
+ (add_at_front ? nodes_to_add : 0); //重新定位新起始节点
if (new_nstart < start.node) //如果新起始节点在原起始节点前
copy(start.node, finish.node + 1, new_nstart); //往前移动中控器现用的节点
else
copy_backward(start.node, finish.node + 1, new_nstart + old_num_nodes); //往后移动中控器现用节点
}
else { //需要扩容
size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2; //至少两倍扩展中控器
map_pointer new_map = map_allocator::allocate(new_map_size); //分配新的中控器
new_nstart = new_map + (new_map_size - new_num_nodes) / 2
+ (add_at_front ? nodes_to_add : 0); //设置新中控器起始节点
copy(start.node, finish.node + 1, new_nstart); //将旧的中控器信息拷贝到新的中控器
map_allocator::deallocate(map, map_size); //释放旧的中控器
/* 更新中控器 */
map = new_map;
map_size = new_map_size;
}
/* 设置好起始迭代器和结尾迭代器对应的中控器节点 */
start.set_node(new_nstart);
finish.set_node(new_nstart + old_num_nodes - 1);
}
首先会获取中控器上已使用的节点数,然后求得新需求的节点数,如果现在中控器上面总的节点数大于新需求节点数的两倍,那么就不需要扩展控制器,只需要修改中控器,否则需要扩展中控器
修改中控器
首先定位中控器新的起始点,如果新的起始点在旧的起始点前面,那么就将中控器内容往前移动,否则,将中控器内容往后移动
扩展中控器
中控器的扩展是以至少两倍的扩展,首先会算出新中控器的节点数,然后为新的中控器分配内存,再设置好新的起始点,将旧的中控器拷贝到新的中控器里,再释放旧的中控器内容,最后设置好deque的中控器map指向
最后,设置好deque的起始迭代器start和结尾迭代器finish就大功告成
push_front
push_front是在开头添加元素,其定义如下
void push_front(const value_type& t) {
if (start.cur != start.first) { //如果缓存区前面空间大于1个元素
construct(start.cur - 1, t); //直接在前面构造对象
--start.cur; //移动迭代器
}
else
push_front_aux(t);
}
push_front跟push_back实现类似,只是方向相反
首先如果缓存区的前面空闲的空间大于一个元素,那么就在缓存区前部构造一个对象,再向前启动起始迭代器start
否则,使用push_front_aux
进行扩容,过程和push_back非常类似,这里不展开讨论
insert
iterator insert(iterator position, const value_type& x) {
if (position.cur == start.cur) {
push_front(x);
return start;
}
else if (position.cur == finish.cur) {
push_back(x);
iterator tmp = finish;
--tmp;
return tmp;
}
else {
return insert_aux(position, x);
}
}
如果在前部插入的话,就调用push_front
,如果在尾部插入的话,就调用push_back
,否则调用insert_aux
下面仔细看一下insert_aux
template <class T, class Alloc, size_t BufSize>
typename deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::insert_aux(iterator pos, const value_type& x) {
difference_type index = pos - start;
value_type x_copy = x;
if (index < size() / 2) { //在前半部插入
...
copy(front2, pos1, front1);
}
else { //在后半部插入
...
copy_backward(pos, back2, back1);
}
*pos = x_copy;
return pos;
}
如果在前半部插入,就将指定位置前面的元素往前移动一个,如果是后半部插入,就将指定元素后面的元素往后移动一个,最后在指定位置填入指定值既可,所以insert
动作的插入效率不是很高
3.4 删除元素
删除元素的操作有两种,一种是pop
,一种是erase
,其中pop
有从首部删除pop_front
,还有从尾部删除pop_back
pop_back
是移除最后一个元素,其定义如下
void pop_back() {
if (finish.cur != finish.first) { //如果析构完之后该缓存区还有对象
--finish.cur;
destroy(finish.cur); //析构
}
else
pop_back_aux();
}
如果缓存区前面还有对象,那么就直接向前移动尾部迭代器finish,然后析构对象
否则,调用pop_back_aux
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>:: pop_back_aux() {
deallocate_node(finish.first); //释放中控器节点对应的缓存区
finish.set_node(finish.node - 1); //设置好迭代器指向的中控器节点
finish.cur = finish.last - 1;
destroy(finish.cur); //析构
}
首先会释放该缓存区,然后移动到上一个缓存区,再析构缓存区最后一个元素
pop_front
pop_front
移除第一个元素,其定义如下
void pop_front() {
if (start.cur != start.last - 1) {
destroy(start.cur);
++start.cur;
}
else
pop_front_aux();
}
如果缓存区后面的元素个数大于1,就直接析构start指向的对象,然后向后移动start迭代器
否则,通过pop_front_aux
来移除首元素,其定义如下
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::pop_front_aux() {
destroy(start.cur);
deallocate_node(start.first);
start.set_node(start.node + 1);
start.cur = start.first;
}
首先析构start指向的对象,然后释放掉该缓存区(因为该缓存区已经没有元素了),然后start移动到下一个缓冲区
erase
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;
}
首先判断,指定点在前半部分还是后半部分
如果在前半部分,那么就将前半部元素往后拷贝,再移除掉首元素
如果在后半部分,那么就将后半部分元素往前拷贝,再移除掉最后一个元素
3.5 其他操作
begin
begin
用于获取首迭代器
iterator begin() { return start; }
end
end
用于获取尾部迭代器
iterator end() { return finish; }
swap
swap
用于交换两个容器,实际上它交换的是中控器还有首尾迭代器
void swap(deque& x) {
__STD::swap(start, x.start);
__STD::swap(finish, x.finish);
__STD::swap(map, x.map);
__STD::swap(map_size, x.map_size);
}