STL源码剖析(十)序列式容器之deque

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中有一个mapmap_size,它们维护着deque的中控器。map其实是一个指针数组,其数组项为value_type*类型,指向一块连续的缓存区,map_size表示该数组项元素个数

另外还有两个迭代器startfinishstart指向首元素,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);
}
发布了107 篇原创文章 · 获赞 197 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42462202/article/details/101764173