STL源码剖析(四)序列式容器--deque

1. 关于deque

1.1 deque概述

  • deque,即我们常说的队列,是一个双向开口的连续线性空间,可以在头尾两端分别做元素的插入和删除操作
  • deque由一段段的定量连续空间组成,稍后再介绍

1.2 deque与vector区别

1. deque允许于常数时间内对起头端进行元素的插入或移除操作
2. deque没有所谓的容量观念
3. 因为deque需要维持其整体连续的假象,导致其实现起来比较繁琐,为了提高效率,尽可能选择vector而不是deque

2. deque构成

– 刚刚说到,deque是由一段段的定量连续空间组成,那么怎样设计才能使得deque看起来整体连续呢?
答案是采用一块map(此map非STLmap)作为主控。map是一小块连续空间,其中每个元素都是指针,指向缓冲区,此缓冲区负责存储deque的元素

在这里插入图片描述

3. deque的迭代器

  • deque迭代器必须要能够指出缓冲区所在位置,其次要能够判断是否在缓冲区的边缘,还须时刻掌握管控中心map
template <class T, class Ref, class Ptr, size_t BufSize>
struct __deque_iterator {
	//...
	static size_t buffer_size() { return  __deque_buf_size(BufSize, sizeof(T)); }  //定义缓冲区大小
	//...
	typedef T** map_pointer;    //管控中心
	T* cur;     //此迭代器所指缓冲区中现行元素
	T* first;   //此迭代器所指缓冲区的头
	T* last;  //此迭代器所指缓冲区的尾
	map_pointer node;    //指向管控中心
	//...
};
//如果n不为0,表示由用户自己设定缓冲区大小,就返回n
//如果n为0,表示采用默认值:
//	如果sz(元素大小)小于512,则缓冲区大小为512 / sz
// 	如果sz不小于512,则返回1
inline size_t __deque_buf_size(size_t n, size_t sz)
{
	return n != 0 ? n : (sz < 512 ? szie_t(512 / sz) : size_t(1));
}
  • 如下图所示,一个迭代器的构造:
    在这里插入图片描述
  • deque迭代器设计即,将所支持的操作运算符重载,比较简单,就不详细分析了

4. deque构造与内存管理

deque,除了维护指向map的指针外,也维护start、finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素,当map所提供的节点不足时,就必须重新分配更大的一块map

  • 以实际例子来解析deque的构造与内存管理:
#include <iostream>
#include <deque>
#include <algorithm>
using namespace std; 

int main(int argc, char** argv) {
 	deque<int> ideq(20,9);    //1
	cout << "size = " << ideq.size() << endl;         //size = 20 
	//为每个元素设定初值  
	for (int i = 0;i < ideq.size(); ++i)
		ideq[i] = i;
	//输出每个元素 
	for (int i = 0; i < ideq.size(); ++i)
		cout << ideq[i] << ' ';                   //0 1 2 ... 19
	cout << endl;
	//在尾端添加3个元素 
	for (int i = 0;i < 3; ++i)       
		ideq.push_back(i);
	//输出所有元素 
	for (int i = 0; i < ideq.size(); ++i)
		cout << ideq[i] << ' ';                   //0 1 2 ...19 0 1 2
	cout << endl;
	cout << "size = " << ideq.size() << endl;     //size = 23
	//在头部增加一个元素 
	ideq.push_front(99);
	for (int i = 0; i < ideq.size(); ++i)
		cout << ideq[i] << ' ';                   //99 0 1 2 ... 19 0 1 2
	cout << endl;
	cout << "size = " << ideq.size() << endl;     //size = 24
	return 0;
}

4.1 deque构造

当创建一个deque时,调用deque的构造函数:

deque(int n, const value_type& value)
	: start(), finsh(), map(0), map_size(0)
{
	fill_initialize(n, value);   //负责安排deque的数据结构,并设定元素初值
}
//fill_initialize函数实现
template <class T, class Alloc, size_t BufSzie>
void deque<T,Alloc, BufSize>::fill_initialize(size_type n, const value_type& value) {
	create_map_and_nodes(n);     //负责安排deque的结构 
	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); 
	}
	catch(...) {
		//...
	}
}
//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)
{
    // 需要的结点数, 元素个数 / 每个缓冲区能容纳的元素数 + 1
  // 这里如果能整除,会多分配一个
  size_type num_nodes = num_elements / buffer_size() + 1;
 
  // map要维护的结点, 这里最小的值为8,最多为所需节点数+2,前后各留一个以便扩充
  map_size = max(initial_map_size(), num_nodes + 2);
  // 调用deque专属空间配置器,配置map空间
  map = map_allocator::allocate(map_size);
 
  // 将[nstart, nfinish)区间设置在map的中间,
  // 这样就能保证前后增长而尽可能减少map的重新分配次数
  map_pointer nstart = map + (map_size - num_nodes) / 2;
  map_pointer nfinish = nstart + num_nodes - 1;
 
  // 分配结点空间
  map_pointer cur;
  __STL_TRY {
    for (cur = nstart; cur <= nfinish; ++cur)
        // 为每一个map指针指向的缓冲区的每一个元素分配内存空间 
      *cur = allocate_node();
  }
 
  // 维护指针状态,为deque的两个迭代器start和finish赋初值
  start.set_node(nstart);
  finish.set_node(nfinish);
  start.cur = start.first;
  finish.cur = finish.first + num_elements % buffer_size();
}

4.2 push_back元素操作

先来简短分析一下push_back的步骤

  1. 调用push_back,如若最后一个缓冲区尚有两个以上的备用空间,则直接在备用空间构造新元素
  2. 只剩一个元素备用空间时,调用push_back_aux配置新缓冲区
  3. 调用push_back_aux,先行调用reserve_map_at_back()判断是否需要更换map,如果需要则调用reallocate_map()进行map的更换,不需要则什么也不做
  4. 在新缓冲区中构造元素并更改finish状态
  • 以下是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);   //配置一块新缓冲区
	}
//只剩一个元素备用空间
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;
  // 判断是否需要调整map空间
  reserve_map_at_back();
  *(finish.node + 1) = allocate_node(); // 配置一块新的缓冲区
  __STL_TRY {
    construct(finish.cur, t_copy);  // 构造新加入的元素
    finish.set_node(finish.node + 1);   // 调整finish
    finish.cur = finish.first;       //令cur指向新缓冲区
  }
  __STL_UNWIND(deallocate_node(*(finish.node + 1)));
}
//reverse_map_at_back()函数实现:
void reverse_map_at_back(size_type nodes_to_add = 1)
{
	if (nodes_to_add + 1 > map_size  - (finish.node - map)  //map尾端备用空间节点不足,则调用下面函数更换map
		reallocate_map(nodes_to_add, false);   //稍后解析
}

4.3 push_front元素操作

先来简短分析一下push_front的步骤

  1. 调用push_front,如若第一缓冲区尚有备用空间,则直接在备用空间构造新元素
  2. 没有元素备用空间时,调用push_front_aux配置新缓冲区
  3. 调用push_front_aux,先行调用reserve_map_at_front()判断是否需要更换map,如果需要则调用reallocate_map()进行map的更换,不需要则什么也不做
  4. 在新缓冲区中构造元素并更改start状态
public:
	void push_front(const value_type& t)  {
		if (start.cur != start.first)  {     //尚有备用空间
			construct(start.cur - 1, t);   //直接构造元素
			--start.cur;         //调整缓冲区使用状态
		}
		else
			push_front_aux(t);   //第一缓冲区无备用空间则调用
	}
//没有备用空间
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_front_aux(const value_type& t)
{
 	value_type t_copy = t;
  	reserve_map_at_front();   // 同push_back(),检查是否需要调整map
  	*(start.node - 1) = allocate_node();  // 配置一块新的缓冲区
  	__STL_TRY {
   		 start.set_node(start.node - 1); // 调整start
   		 start.cur = start.last - 1;
  		  construct(start.cur, t_copy);
 	 }
  	catch (...) {
   		  start.set_node(start.node + 1);
 		  start.cur = start.first;
		  deallocate_node(*(start.node - 1));
  		  throw;
	}
}
//reserve_map_at_front函数实现
void reverse_map_at_front(size_type nodes_to_add = 1)  {
	if (nodes_to_add > start.node - map)  //map前端节点不足则调用下面函数进行更换map
		reallocate_map(nodes_to_add, true);
}

4.4 reallocate_map函数实现

主要分两种情况进行分析:
1. 如果只是一端的节点使用完了,而另一端还剩余很多空间,则在原map进行调整
2. 实在没有空间可供调整,那就配置一块新map,将原map的内容拷贝过来,释放掉原来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) {
    // 调整新的map中的起始点
    new_nstart = map + (map_size - new_num_nodes) / 2
                 + (add_at_front ? nodes_to_add : 0);  //根据传入的bool值进行调整
    // 如果前端剩余很多
    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 {    // map不够用了,就需要配置一块更大的map
    size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2;
    // 配置一块大的map
    map_pointer new_map = map_allocator::allocate(new_map_size);
    // 始终要使start和finish处在map空间的中间
    new_nstart = new_map + (new_map_size - new_num_nodes) / 2
                 + (add_at_front ? nodes_to_add : 0);
    // 拷贝到新的map空间中去
    copy(start.node, finish.node + 1, new_nstart);
    // 释放旧的空间
    map_allocator::deallocate(map, map_size);
    // 设定map的起始地址和大小
    map = new_map;
    map_size = new_map_size;
  }
  // 调整新的start和finish
  start.set_node(new_nstart);
  finish.set_node(new_nstart + old_num_nodes - 1);
}

5. deque的元素操作

  • 以下是一些元素操作,push_back与push_front上述已经解析了,所以在这就不再重复提及:
操作 功能
pop_back 将尾端元素去掉,若最后缓冲区没有元素则会调用pop_back_aux进行缓冲区的释放
pop_front 将首部元素去掉,若第一缓冲区只有一个元素则调用pop_front_aux进行缓冲区的释放
clear 用来清除整个deque,会保留一个缓冲区
erase 1.清除某个元素,返回被删元素位置的迭代器 2.用来清除[first,last)之间的元素
insert 1.若插入点是deque的最前端,交给push_front去做 2.如果插入点是最尾端,则交给push_back去做 3. 否则交给insert_aux

5.1 关于erase

首先理清以下erase的思路:
erase单个元素:
1. 如果清除点之前的元素较少,则向后移动清除点之前的元素
2. 如果清除点之后的元素较少,则向前移动清除点之后的元素

erase[first,last)区间元素:
1. 清除的是整个deque,则调用clear
2. 清除的是部分空间:
2.1 如果清除点之前的元素较少,则向后移动清除点之前的元素
2.2 如果清除点之后的元素较少,则向前移动清除点之后的元素

  • 以下是函数实现:
//清除单个元素
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;
}
//清除一个区间
template <class T, class Alloc, size_t BufSize>
deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::erase(iterator first, iterator last)
{
  if (first == start && last == finish) {   // 需要擦除整个deque
    clear();
    return finish;
  }
  else {
    difference_type n = last - first;   // 清除区间的长度
    difference_type elems_before = first - start;  // 待清除区间前方的元素个数
    if (elems_before < (size() - n) / 2) {  // 如果前方的元素个数较少
      copy_backward(start, first, last);    // 向后移动前方元素
      iterator new_start = start + n;     // 调整新的起始点
      destroy(start, new_start);          // 全局函数,析构节点元素
      for (map_pointer cur = start.node; cur < new_start.node; ++cur)
        data_allocator::deallocate(*cur, buffer_size());   // 释放缓冲区空间
      start = new_start;
    }
    else {    // 后方元素比较少的情况
      copy(last, finish, first);    // 向前移动后方元素
      iterator new_finish = finish - n; // 调整新的finish迭代器
      destroy(new_finish, finish);      // 全局函数,析构节点元素
      for (map_pointer cur = new_finish.node + 1; cur <= finish.node; ++cur)
        data_allocator::deallocate(*cur, buffer_size());  // 释放缓冲区空间
      finish = new_finish;
    }
    return start + elems_before;
  }
}

5.2 关于insert_aux

实现原理:
1. 判断插入点之前的元素个数
2. 如插入点之前的元素个数较少,则将插入点之前的元素(包括插入点的元素)向后移动一位,将新元素x插入到插入点
3. 如插入点之后的元素个数较少,则将插入点之后的元素(包括插入点的元素)向前移动一位,将新元素x插入到插入点

  • 以下是函数实现:
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) {  // 如果前端的元素比较少
    push_front(front());  // 在最前面插入一个与第一个元素一样的数
    iterator front1 = start;  // 记录起始点
    ++front1;
    iterator front2 = front1; 
    ++front2;
    pos = start + index;
    iterator pos1 = pos;
    ++pos1;
    copy(front2, pos1, front1); // 拷贝空间,将[front2,pos1)拷贝到front1
  }
  else {   // 后端的元素比较少,原理同上
    push_back(back());
    iterator back1 = finish;
    --back1;
    iterator back2 = back1;
    --back2;
    pos = start + index;
    copy_backward(pos, back2, back1);  //拷贝空间,将[pos, back2]拷贝到back1
  }
  *pos = x_copy;
  return pos;
}

猜你喜欢

转载自blog.csdn.net/qq_38790716/article/details/84332238