1. 程式人生 > >帶你深入理解STL之Vector容器

帶你深入理解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函式

有上述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容器到此就介紹完畢了。其中對改善效率作了不少優化,很多設計點都值得學習!若有疑惑可以在博文下方留言,我看到會及時幫大家解答!

參考: