主流的STL容器的數據結構都比較常規,類似List就是實現了鏈表的數據結構,數據以一個node接連串接一個node的形式存儲;vector則是一個連續空間存儲的變長數組,當空間用完後則申請一倍的空間並老數據拷貝到新分配空間中;map和set則是紅黑樹,一種特殊的二叉平衡搜索樹,保證根到葉節點最大的層數差距在兩倍以內,以此保證搜索的速度和維護樹結構的性能平衡;unordered_map和unordered_set則使用哈希表,有不同的哈希函數,也有不同的桶實現方法,在此不予細究;唯獨deque的實現略“非主流”,這裏詳細結合STL中的實現解析一下。
基本原理概述
侯捷的《STL源碼剖析》中有詳細的理論解析,只是示例代碼有點老,在此截圖一用:
上圖可以明顯的看出,deque對於數據的中的存儲並不是嚴格連續的,而是分成了很多個緩衝區,用一個map(並非STL中map,而只是一個簡單的連續數組)存儲所有緩衝區的地址指針,每個緩衝區都可以存儲多個數據,當一個緩衝區填滿以後,會開闢新的緩衝區存儲數據,並將其地址指針存入map,若map也已經存滿以後,會創建新的更大的map,並將舊的map數據拷貝進去並釋放舊的空間。
代碼分析
下面結合代碼來詳細看看細節(代碼來自gcc5.4自帶的STL,stl_deque.h):
template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
class deque : protected _Deque_base<_Tp, _Alloc>
deque繼承自_Deque_base類,空間分配器默認爲std標準分配器allocator(該版本底層實現爲new和delete的封裝,而並非一級二級配置器的模式),_Deque_base類主要完成了deque的內存管理相關的任務,拿一些主要的部分來看:
template<typename _Tp, typename _Alloc>
class _Deque_base
{
protected:
typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
rebind<_Tp>::other _Tp_alloc_type; //確定_Tp空間分配器類型
typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type> _Alloc_traits; //alloc_traits會實際調_Tp_alloc_type來分配內存和完成構造和析構操作,這裏不詳細分析
typedef typename _Alloc_traits::pointer _Ptr;
typedef typename _Alloc_traits::const_pointer _Ptr_const;
typedef typename _Alloc_traits::template rebind<_Ptr>::other
_Map_alloc_type; //確定_Map空間分配器類型
typedef __gnu_cxx::__alloc_traits<_Map_alloc_type> _Map_alloc_traits; //同上
...
以上代碼主要用來確定空間分配器類型,之後的接口中會利用以上確定的空間分配器分配空間以及構造對象,再看看主要成員和接口:
struct _Deque_impl
: public _Tp_alloc_type
{
_Map_pointer _M_map; //上文提到的map地址
size_t _M_map_size; //map大小
iterator _M_start; //首元素所在map節點迭代器
iterator _M_finish; //尾後元素所在map節點迭代器
...
}
_Deque_impl _M_impl;
實現了一個內置類型,繼承自_Tp空間分配器類,定義一個其對象並作爲空間分配器,之後所有空間分配任務和構造析構任務都由這個對象完成,同時包含一些重要成員。
_Ptr
_M_allocate_node()
{
typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type> _Traits;
return _Traits::allocate(_M_impl, __deque_buf_size(sizeof(_Tp)));
}
void
_M_deallocate_node(_Ptr __p) _GLIBCXX_NOEXCEPT
{
typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type> _Traits;
_Traits::deallocate(_M_impl, __p, __deque_buf_size(sizeof(_Tp)));
}
_Deque_base僅僅只包涵了分配空間和回收的接口,這裏只拿了node(上圖中的緩衝器)相關的接口,還有map的接口,都大同小異,都是直接調用空間分配起來分配空間。最後提一下_Deque_base的構造中會用到的_M_initialize_map(size_t __num_elements)函數:
template<typename _Tp, typename _Alloc>
void
_Deque_base<_Tp, _Alloc>::
_M_initialize_map(size_t __num_elements)
{
const size_t __num_nodes = (__num_elements/ __deque_buf_size(sizeof(_Tp))
+ 1); //根據初始化元素的個數確定需要的map大小
this->_M_impl._M_map_size = std::max((size_t) _S_initial_map_size,
size_t(__num_nodes + 2)); //根據上面確定map的大小決定(最小爲_S_initial_map_size=8)
this->_M_impl._M_map = _M_allocate_map(this->_M_impl._M_map_size); //分配map的空間
// For "small" maps (needing less than _M_map_size nodes), allocation
// starts in the middle elements and grows outwards. So nstart may be
// the beginning of _M_map, but for small maps it may be as far in as
// _M_map+3.
_Map_pointer __nstart = (this->_M_impl._M_map
+ (this->_M_impl._M_map_size - __num_nodes) / 2); //因爲是雙端隊列map都是從中間開始分配空間,首元素向左生長,尾元素向右,這點比較重要
_Map_pointer __nfinish = __nstart + __num_nodes;
//以下給緩衝器分配空間
__try
{ _M_create_nodes(__nstart, __nfinish); }
__catch(...)
{
_M_deallocate_map(this->_M_impl._M_map, this->_M_impl._M_map_size);
this->_M_impl._M_map = _Map_pointer();
this->_M_impl._M_map_size = 0;
__throw_exception_again;
}
//以下就是空間分配完成後初始化_M_impl中各個關鍵元素,上文已經提過相應意義
this->_M_impl._M_start._M_set_node(__nstart);
this->_M_impl._M_finish._M_set_node(__nfinish - 1);
this->_M_impl._M_start._M_cur = _M_impl._M_start._M_first;
this->_M_impl._M_finish._M_cur = (this->_M_impl._M_finish._M_first
+ __num_elements
% __deque_buf_size(sizeof(_Tp)));
}
到這裏_Deque_base的任務基本就完成了,它所有的任務就是提供創建、回收map和node的接口,方便後面的使用。
然後來看看deque的主要實現,主要就是完成元素的插入刪除,以插入爲例子:
void
push_back(const value_type& __x)
{
if (this->_M_impl._M_finish._M_cur
!= this->_M_impl._M_finish._M_last - 1)
{
_Alloc_traits::construct(this->_M_impl,
this->_M_impl._M_finish._M_cur, __x);
++this->_M_impl._M_finish._M_cur;
}
else
_M_push_back_aux(__x);
}
邏輯還是比較簡單的,if判斷該緩衝區還有沒有空間,有就直接創建對象返回,沒有就調用_M_push_back_aux(__x),看看它的實現:
void
deque<_Tp, _Alloc>::
_M_push_back_aux(_Args&&... __args)
{
_M_reserve_map_at_back(); // 1
*(this->_M_impl._M_finish._M_node + 1) = this->_M_allocate_node(); // 2
__try
{
_Alloc_traits::construct(this->_M_impl, // 3
this->_M_impl._M_finish._M_cur,
std::forward<_Args>(__args)...);
this->_M_impl._M_finish._M_set_node(this->_M_impl._M_finish._M_node
+ 1);
this->_M_impl._M_finish._M_cur = this->_M_impl._M_finish._M_first;
}
__catch(...)
{
_M_deallocate_node(*(this->_M_impl._M_finish._M_node + 1));
__throw_exception_again;
}
}
_M_push_back_aux(__x)也並不複雜,因爲目前尾元素所在緩衝區空間不夠了,因此需要申請新的空間,1、_M_reserve_map_at_back()實現的主要功能就是檢查map空間是否足夠,不夠就申請新的空間,並將數據移動過去,並更新_M_impl中存儲關鍵元素的信息;2、然後根據尾元素所在節點申請新的緩衝區空間;3、最後構造元素。
push_front()基本完全一致,只不過其生長方向與back完全相反,可以簡單理解爲front和back從初始的中間位置分別向兩端生長,一旦所在緩衝區空間不夠就申請新的空間,並在新的空間繼續生長,最後看下pop_back函數,front類似:
void
pop_back() _GLIBCXX_NOEXCEPT
{
if (this->_M_impl._M_finish._M_cur
!= this->_M_impl._M_finish._M_first)
{
--this->_M_impl._M_finish._M_cur;
_Alloc_traits::destroy(this->_M_impl,
this->_M_impl._M_finish._M_cur);
}
else
_M_pop_back_aux();
}
可以看到,基本就是push_front反過來,if檢查當前元素是否到達當前緩衝區最前端,不在則直接pop,否則調用_M_pop_back_aux(),_M_pop_back_aux()與_M_push_back_aux()也是幾乎完全相反,需要注意的是它會回收緩衝區內存,其他的這裏就不再贅述了。
總結
總體來看,deque由於結構的原因,它整體的實現比vector要複雜很多,緩衝區有點類似於一個每個“節點”都是一個大block的List,只不過它的操作主要在頭尾兩端,頭尾節點一旦空間不夠了就新建“節點”,pop空了就刪除“節點”,因此如果不想要頻繁在頭部操作時,選擇vector要遠遠優於deque;但如果有大量頭部操作時(例如FIFO)還是老老實實用deque吧。