帶你深入理解STL之Vector容器
C++內建了陣列的型別,在使用陣列的時候,必須指定陣列的長度,一旦配置了就不能改變了,通常我們的做法是:儘量配置一個大的空間,以免不夠用,這樣做的缺點是比較浪費空間,預估空間不當會引起很多不便。
STL實現了一個Vector容器,該容器就是來改善陣列的缺點。vector是一個動態空間,隨著元素的加入,它的內部機制會自行擴充以容納新元素。因此,vector的運用對於記憶體的合理利用與運用的靈活性有很大的幫助,再也不必因為害怕空間不足而一開始就配置一個大容量陣列了,vector是用多少就分配多少。
要想實現動態分配陣列,Vector內部就需要對空間控制做到有效率的掌控,這些機制要如何運作才能高效地實現動態分配呢?本篇部落格就從原始碼的角度帶你領略一下Vector容器內部的構造藝術。
Vector概述
大家知道,初始化一個數組的時候,需要給陣列分配一塊記憶體,陣列中的資料都是按序存放的。vector也是如此,再初始化的時候給vector容器分配一塊記憶體,用來存放容器中的資料,一旦分配的記憶體不足以存放新加入的資料時,就需要擴充空間。STLVector的做法是:重新開闢一段新的空間,將原空間的資料遷移過去,然後新加入的資料存放在新空間之後並釋放掉原有空間。
在這個過程中,配置新空間->資料移動->釋放舊空間會帶來一定的時間成本,所以必須儘可能高效的實現,STL的Vector設計中對這一部分做了相當大的優化,使得時間成本儘可能的小。下面就一起去看看這些優秀的設計吧↓。
Vector的資料結構
我們從最簡單的開始,Vector的資料結構相當簡單,由於需要判斷記憶體是否夠用,所以要用到三個指標,分別指向頭,目前使用空間的尾,目前可用空間的尾。其原始碼如下:
template <class T, class Alloc = alloc>//alloc是STL的空間配置器
class vector
{
// 這裡提供STL標準的allocator介面
typedef simple_alloc<value_type, Alloc> data_allocator;
iterator start; // 記憶體空間起始點
iterator finish; // 當前使用的記憶體空間結束點
iterator end_of_storage; // 實際分配記憶體空間的結束點
}
每當初始化一個vector的時候,先分配一段記憶體,稱為目前可用空間,大小為end_of_storage - start + 1,當往vector裡面加入資料的時候,finish就往後移,代表目前已使用的空間,這樣做的好處是,不用頻繁的擴充空間和轉移資料,使得時間成本下降。
在上述程式碼中,我們看到vector採用了STL標準的空間配置其介面,關於空間配置器的知識在帶你深入理解STL之空間配置器(思維導圖+原始碼)一文中有講解,如有疑惑,可以跳轉複習一下再來!
vector提供瞭如下函式來支援獲取其資料結構中的相關引數。
//獲取指向vector首元素的迭代器
iterator begin() { return start; }
//獲取指向vector尾元素的迭代器
iterator end() { return finish; }
// 返回當前物件個數,即已使用空間的大小
size_type size() const { return size_type(end() - begin()); }
// 返回重新分配記憶體前最多能儲存的物件個數,即目前可用空間的大小
size_type capacity() const { return size_type(end_of_storage - begin()); }
Vector的迭代器
既然是STL的容器,必須要滿足迭代器的相關要求,如對迭代器有疑惑的,參考帶你深入理解STL之迭代器和Traits技法
。
vector維護的是一段連續的記憶體空間,所以不論容器中元素的型別為何,普通指標都可以作為vector的迭代器而滿足所有必要的條件。vector支援隨機存取,所以vector提供的是Random Access Iterator。
下面來看看vector關於迭代器的原始碼:
template <class T, class Alloc = alloc>
class vector
{
public:
// vector內部是連續記憶體空間,所以迭代器採用原生指標即可
typedef value_type* iterator;
//以下為滿足Traits功能定義的內嵌型別
typedef T value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef const value_type* const_iterator;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef ptrdiff_t difference_type;
typedef size_t size_type;
}
vector的建構函式
預設建構函式
在使用vector的時候,我們通常會有如下定義:
#include <vector>
vector<int> vec;
在上述定義中,呼叫了vector的預設建構函式,其預設不分配記憶體空間,如下:
// vector的預設建構函式預設不分配記憶體空間
vector() : start(0), finish(0), end_of_storage(0) {}
帶參建構函式
通常,vector的初始化可以指定元素個數和初始化型別。如下:
vector<int> vec(10,1); // 將vec初始化為10個1
vector提供下面的建構函式以支援上述初始化操作:
// 建構函式,允許指定vector的元素個數和初值
vector(size_type n, const T& value) { fill_initialize(n, value); }
vector(int n, const T& value) { fill_initialize(n, value); }
vector(long n, const T& value) { fill_initialize(n, value); }
// 需要物件提供預設建構函式
explicit vector(size_type n) { fill_initialize(n, T()); }
/**
* 填充並予以初始化
*/
void fill_initialize(size_type n, const T& value)
{
start = allocate_and_fill(n, value);
finish = start + n; // 設定當前使用記憶體空間的結束點
//這裡不過多的分配記憶體
end_of_storage = finish;
}
/**
* 配置一塊大小為n的記憶體空間,並予以填充
*/
iterator allocate_and_fill(size_type n, const T& x)
{
// 呼叫STL的空間配置器配置一塊大小為n的記憶體空間
iterator result = data_allocator::allocate(n);
// 呼叫底層函式uninitialized_fill_n予以填充
uninitialized_fill_n(result, n, x);
return result;
}
這裡面呼叫了uninitialized_fill_n函式,這個函式是STL的記憶體基本處理函式,存放在stl_uninitialized.h中,下面來看看它的原始碼:
// 如果copy construction和operator =等效, 並且destructor is trivial
// 那麼就可以使用本函式
template <class ForwardIterator, class Size, class T>
inline ForwardIterator
__uninitialized_fill_n_aux(ForwardIterator first, Size n,
const T& x, __true_type)
{
return fill_n(first, n, x);
}
// 不是POD型別使用以下函式
template <class ForwardIterator, class Size, class T>
ForwardIterator
__uninitialized_fill_n_aux(ForwardIterator first, Size n,
const T& x, __false_type)
{
ForwardIterator cur = first;
for ( ; n > 0; --n, ++cur)
construct(&*cur, x);
return cur;
}
// 利用type_traits來判斷是否是POD型別
template <class ForwardIterator, class Size, class T, class T1>
inline ForwardIterator __uninitialized_fill_n(ForwardIterator first, Size n,
const T& x, T1*)
{
typedef typename __type_traits<T1>::is_POD_type is_POD;
return __uninitialized_fill_n_aux(first, n, x, is_POD());
}
// 利用Iterator_traits來萃取出其值型別
template <class ForwardIterator, class Size, class T>
inline ForwardIterator uninitialized_fill_n(ForwardIterator first, Size n,
const T& x)
{
return __uninitialized_fill_n(first, n, x, value_type(first));
}
vector的元素操作函式
push_back()
push_back()函式將新元素插入於vector的尾部,該函式再完成這一操作的時候,先檢查是否還有備用空間,如果有直接再備用空間上建構函式;如果沒有就擴充空間,通過重新配置一塊大空間,移動資料,釋放原空間的操作來完成push_back操作。其原始碼如下:
////////////////////////////////////////////////////////////////////////////////
// 向容器尾追加一個元素, 可能導致記憶體重新分配
////////////////////////////////////////////////////////////////////////////////
// push_back(const T& x)
// |
// |---------------- 容量已滿?
// |
// ----------------------------
// No | | Yes
// | |
// ↓ ↓
// construct(finish, x); insert_aux(end(), x);
// ++finish; |
// |------ 記憶體不足, 重新分配
// | 大小為原來的2倍
// new_finish = data_allocator::allocate(len); <stl_alloc.h>
// uninitialized_copy(start, position, new_start); <stl_uninitialized.h>
// construct(new_finish, x); <stl_construct.h>
// ++new_finish;
// uninitialized_copy(position, finish, new_finish); <stl_uninitialized.h>
////////////////////////////////////////////////////////////////////////////////
void push_back(const T& x)
{
// 記憶體滿足條件則直接追加元素, 否則需要重新分配記憶體空間
if (finish != end_of_storage) {
construct(finish, x);
++finish;
}
else
insert_aux(end(), x);
}
////////////////////////////////////////////////////////////////////////////////
// 提供插入操作
////////////////////////////////////////////////////////////////////////////////
// insert_aux(iterator position, const T& x)
// |
// |---------------- 容量是否足夠?
// ↓
// -----------------------------------------
// Yes | | No
// | |
// ↓ |
// 從opsition開始, 整體向後移動一個位置 |
// construct(finish, *(finish - 1)); |
// ++finish; |
// T x_copy = x; |
// copy_backward(position, finish - 2, finish - 1); |
// *position = x_copy; |
// ↓
// data_allocator::allocate(len);
// uninitialized_copy(start, position, new_start);
// construct(new_finish, x);
// ++new_finish;
// uninitialized_copy(position, finish, new_finish);
// destroy(begin(), end());
// deallocate();
////////////////////////////////////////////////////////////////////////////////
template <class T, class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T& x)
{
if (finish != end_of_storage) { // 還有剩餘記憶體
construct(finish, *(finish - 1));
++finish;
T x_copy = x;
copy_backward(position, finish - 2, finish - 1);
*position = x_copy;
}
else {
// 記憶體不足, 需要重新分配
const size_type old_size = size();
//配置原則:如果原大小為0,就配置1個元素大小
// 如果原大小不為0,就配置原大小的兩倍
// 前半段用來放置原資料,後半段用來放置新資料
const size_type len = old_size != 0 ? 2 * old_size : 1;
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
// 將記憶體重新配置
__STL_TRY {
// 將原vector的內容拷貝到新vector
new_finish = uninitialized_copy(start, position, new_start);
// 構造新元素並賦值為x
construct(new_finish, x);
// 調整finish的位置
++new_finish;
// 將安插點的原內容也拷貝過來
new_finish = uninitialized_copy(position, finish, new_finish);
}
// 分配失敗則丟擲異常
catch (...) {
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
// 析構原容器中的物件
destroy(begin(), end());
// 釋放原容器分配的記憶體
deallocate();
// 調整記憶體指標狀態
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
pop_back()函式
pop_back函式彈出當前尾端元素。其原始碼比較簡單,如下:
void pop_back()
{
//調整finish
--finish;
//釋放調彈出的元素
destroy(finish);
}
erase()函式
erase函式支援兩個版本:
- 清除某個位置上的元素
iterator erase(iterator position)
{
if (position + 1 != end())
copy(position + 1, finish, position); //將[position+1,finish]移到[position,finish]
--finish;
destroy(finish);
return position;//返回刪除點的迭代器
}
- 清除某個區間上的所有函式
iterator erase(iterator first, iterator last)
{
iterator i = copy(last, finish, first);//關於copy函式的原始碼分析在以後的博文中會提到
// 析構掉需要析構的元素
destroy(i, finish);
finish = finish - (last - first);
return first;
}
這裡放上兩張《STL原始碼剖析》中的圖,便於理解這一過程:
有上述erase函式,可以衍生出一個函式,用來清除迭代器中所有的元素
void clear() { erase(begin(), end()); }
insert()函式
insert函式實現的功能是:從position開始,插入n個元素,元素的初值均為x。其原始碼如下:
////////////////////////////////////////////////////////////////////////////////
// 在指定位置插入n個元素
////////////////////////////////////////////////////////////////////////////////
// insert(iterator position, size_type n, const T& x)
// |
// |---------------- 插入元素個數是否為0?
// ↓
// -----------------------------------------
// No | | Yes
// | |
// | ↓
// | return;
// |----------- 記憶體是否足夠?
// |
// -------------------------------------------------
// Yes | | No
// | |
// |------ (finish - position) > n? |
// | 分別調整指標 |
// ↓ |
// ---------------------------- |
// No | | Yes |
// | | |
// ↓ ↓ |
// 插入操作, 調整指標 插入操作, 調整指標 |
// ↓
// data_allocator::allocate(len);
// new_finish = uninitialized_copy(start, position, new_start);
// new_finish = uninitialized_fill_n(new_finish, n, x);
// new_finish = uninitialized_copy(position, finish, new_finish);
// destroy(start, finish);
// deallocate();
////////////////////////////////////////////////////////////////////////////////
template <class T, class Alloc>
void vector<T, Alloc>::insert(iterator position, size_type n, const T& x)
{
// 如果n為0則不進行任何操作
if (n != 0) {
if (size_type(end_of_storage - finish) >= n) { // 剩下的記憶體夠分配
T x_copy = x;
const size_type elems_after = finish - position; // 計算插入點之後的現有元素個數
iterator old_finish = finish;
if (elems_after > n) { // 插入點之後的現有元素個數大於新增元素個數,見下圖1
// 先複製尾部n個元素到尾部
uninitialized_copy(finish - n, finish, finish);
finish += n; // 調整新的finish
// 從後往前複製剩餘的舊元素
copy_backward(position, old_finish - n, old_finish);
// 從position開始填充新元素
fill(position, position + n, x_copy);
}
else {
// 插入點之後的現有元素個數小於新增元素個數,見下圖2
// 先在尾部填充n - elems_after個新增元素
uninitialized_fill_n(finish, n - elems_after, x_copy);
// 調整新的finish
finish += n - elems_after;
// 複製[position,old_finish]區間的數到新的finish之後
uninitialized_copy(position, old_finish, finish);
// 調整finish
finish += elems_after;
// 從position開始填充新增元素
fill(position, old_finish, x_copy);
}
}
else { // 剩下的記憶體不夠分配, 需要重新分配
const size_type old_size = size();
const size_type len = old_size + max(old_size, n);
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
__STL_TRY {
// 將舊的vector中插入點之前的元素複製到新空間,見下圖3
new_finish = uninitialized_copy(start, position, new_start);
// 將新增元素複製到新空間
new_finish = uninitialized_fill_n(new_finish, n, x);
// 將插入點之後的元素複製到新空間
new_finish = uninitialized_copy(position, finish, new_finish);
}
catch (...) {
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
// 清除並釋放原有vector
destroy(start, finish);
deallocate();
// 調整新的start和finish
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
}
上述操作可以使插入操作達到最高的效率。配合以下圖解更容易理解:
插入點之後的現有元素個數大於新增元素個數的情況
插入點之後的現有元素個數小於新增元素個數的情況
剩下的記憶體不夠分配,重新配置的情況
後記
STL的Vector容器到此就介紹完畢了。其中對改善效率作了不少優化,很多設計點都值得學習!若有疑惑可以在博文下方留言,我看到會及時幫大家解答!
參考: