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);
}