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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章