【決戰西二旗】|理解STL vector原理

Vector概述

STL中的Vector是最爲經常使用的順序容器之一,與C/C++中的數組Array很類似,最重要的區別在於對連續內存空間運用的靈活性。git

數組真叫人爲難

咱們在使用數組Array時要指定數組的大小,然後由系統來分配連續的內存空間,Array是靜態空間必須指定大小且指定以後自動沒法擴縮容,所以爲了不元素裝不下,通常都把數組設置的比預期要大,可是每每致使空間的浪費。github

另外每每不少時候需裝載元素的數量沒法預期,這樣設置怎樣的大小就取決於使用者了。若是最初數組設計比較小,那麼就面臨着開闢新的更大空間、將舊空間元素複製到新空間、釋放舊空間三個大步驟,這一切都是須要使用者本身來維護實現的。數組

使用數組太大浪費空間、過小浪費效率,真的很但願有個助手幫咱們作了這些事情,專業的人作專業的事情,讓咱們只關心業務邏輯便可,那該多好!bash

它來了,踏着七彩祥雲

就這樣盼望着 盼望着 東風來了 春天的腳步近了,不不, vector的腳步近了….函數

vector底層與Array同樣都是連續的內存空間,區別在於vector的空間是動態的,隨着更多元素的加入能夠自動實現空間擴展,而且vector針對這種擴展作了優化,並非one by one的擴展,那樣實在是低效,而是按照某種倍率來擴展,這樣就有效了減小由於擴容帶來的複製效率下降問題。優化

簡單來講就是當須要放置1個元素時,vector空間已滿,此時vector並不會只向系統申請1個元素的空間,而是按照目前已佔用的空間的倍率來申請。ui

假如原來佔用A字節,那麼再次申請時多是2A字節,因爲此時向尾部地址擴展不必定有連續未分配的內存,大多時候仍是會涉及開闢新的更大空間、將舊空間元素複製到新空間、釋放舊空間三個大步驟。spa

因此和數組相比底層的操做都是同樣的,不要把Vector神話,都是普通的結構只不過被封裝了一層而已。設計

從本質上看,vector就是在普通Array和使用者中間加了一層,從而把使用者從對數組的直接管理權接手過來,讓使用者有個管家同樣,在毫無影響使用的前提下更加省心和高效。指針

透過現象看本質

  • 空間分配

看中對vector的定義(節選部分紅員變量)

//alloc是SGI STL的空間配置器
template <class T,class Alloc=alloc> class vector {
public:
    typedef T   value_type;
    typedef value_type*  pointer;
    typedef value_type*  iterator;
    typedef value_type&  reference;
    typedef size_t   size_type;
    typedef ptrdiff_t   difference_type;

protected:
    //simple_alloc是SGI STL的空間配置器
    typedef simple_alloc<value_type,Alloc> data_allocator;
    //表示目前使用空間的頭
    iterator start; 
    //表示目前使用空間的尾
    iterator finish; 
    //表示目前可用空間的尾
    iterator end_of_storage;
}複製代碼

從定義看vector 是一種前閉後開的容器,在vector裏頭有一些變量記錄着一些信息,分別是三個迭代器:

  1. start:表示目前使用空間的頭
  2. finish:目前使用空間的尾
  3. end_of_storage:目前可用空間的尾

vector的空間示意圖:

vector中有三個和容量空間相關的成員函數:size、capacity、empty。

其中size表明已存儲的元素數量,capacity表示能夠容納的最大元素數量,empty判斷是否無元素存儲,這些判斷也都是依據迭代器來實現的,定義以下:

size_type size() const {return size_type(end()-begin());}
size_type capacity() const{return size_type(end_of_storage-begin());}
bool empty() const{return begin()==end();}複製代碼

  • 迭代器

vector因爲在底層和數組沒有區別,都是地址連續的內存空間,所以普通指針具有vector迭代器的重要功能,諸如:
*、->、++、--、+、-、+=、-=
這些迭代器操做,均可以由指針來實現。

//迭代器舉例
iterator begin()
{
    return start;
}
iterator end()
{
    return finish;
}

reference front()
{
    return *begin();
}
reference back()
{
    return *(end() - 1);
}複製代碼

  • push_back

在使用push_back()將元素插入vector尾端時的基本流程:

  1. 仍有空間 直接在剩餘空間上構造元素,並調整迭代器finish指向
  2. 已無空間 擴充空間(從新分配、移動數據、釋放舊空間)
  3. 擴充空間時調用另一個成員函數insert_aux來實現,其中咱們能夠看到更多的細節。

代碼定義以下:

void push_back(const T& x)//添加元素 {
    //是否超出最大可容納空間
    if(finish !=end_of_storage){
        /*全局函數,construct()接收一個指針p和一個初值value, 該函數的用途就是將 初值value設定到指針鎖指的空間上。 */
        construct(finish,x);
        ++finish;
    }
    else{
        insert_aux(end(),x);  //vector的成員函數
    }
}

template <class T, class Alloc = alloc> void vector<T, Alloc>::insert_aux(iterator position, const T& x) {
    if (finish != end_of_storage) {
        //在備用空間起始處構造一個元素,並以vector最後一個元素值爲其初始值
        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();
        const size_type len = old_size != 0 ? 2 * old_size : 1;
        //以上配置原則:若是原大小爲0,則配置1(個元素大小);
        //若是原大小不爲0, 則配置原大小的兩倍,
        //前半段用來放置原數據,後半段準備用來放置新數據

        iterator new_start = data_allocator::allocate(len); //實際配置
        iterator new_finish = new_start;
        try {
            //將原vector的內容拷貝到新vector
            new_finish = uninitialized_copy(start, position, new_start);
            //將新元素設定初值x
            construct(new_finish, x);
            //調整水位
            ++new_finish;
            //將安插點的原內容也拷貝進來(提示:本函數也可能被insert(p,x)調用)
            new_finish = uninitialized_copy(position, finish, new_finish);
        }
        catch(...) {
            //"commit or rollback" semantics
            destroy(new_start, new_finish);
            data_allocator::deallocate(new_start, len);
            throw;
        }

        //析構並釋放原vector
        destroy(begin(), end());
        deallocate();

        //調整迭代器,指向新vector
        start = new_start;
        finish = new_finish;
        end_of_storage = new_start + len;
    }
}複製代碼

  • insert

insert成員函數也是實現元素添加的主要函數,所以也涉及了與push_back相似的擴容過程,可是比push_back更復雜一些,在插入元素時須要根據插入點、備用空間大小、待插入元素數量等角度進行分類對待,詳細過程看下代碼定義:

//從position開始,插入n個元素,元素初值爲x
template <class T, class Alloc> void vector<T, Alloc>::insert(iterator position, size_type n, const T& x)
{
    if ( n != 0){   //當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) {
                //「插入點以後的現有元素個數」大於「新增元素個數」
                uninitialized_copy(finish - n, finish, finish);
                finish += n;    //將vector尾端標記後移
                copy_backward(position, old_finish - n, old_finish);    //逆向拷貝
                fill(position, position + n, x_copy);
            }
            else {
                //「插入點以後的現有元素個數」小於等於「新增元素個數」
                uninitialized_fill_n(finish, n - elems_after, x_copy);
                finish += n-elems_after;
                uninitialized_copy(position, old_finish, finish);
                finish += elems_after;
                fill(position, old_finish, x_copy);
            }
        }
        else {
            //備用空間小於「新增元素個數」(那就必須配置額外內存)
            //首先決定新長度:舊長度的兩倍,或舊長度+新增元素個數
            const size_type old_size = size();
            const size_type len = old_size + max(old_size, n);
            //如下配置新的vector空間
            iterator new_start = data_allocator::allocate(len);
            iterator new_finish = new_start;
            __STL_TRY {
                //如下首先將舊vector的插入點以前的元素複製到新空間
                new_finish = uninitialized_copy(start, position, new_start);
                //如下再將新增元素(初值皆爲n)填入新空間
                new_finish = uninitialized_fill_n(new_finish, n, x);
                //如下再將舊vector的插入點以後的元素複製到新空間
                new_finish = uninitialized_copy(position, finish, new_finish);
            }

        #ifdef __STL_USE_EXCEPTIONS
            catch(...) {
                //若有異常發生,實現」commit or rollback"semantics
                destroy(new_start, new_finish);
                data_allocator::deallocate(new_start, len);
                throw;
            }
        #endif /* __STL_USE_EXCEPTIONS */
        //如下清楚並釋放舊的vector
        destroy(start, finish);
        deallocate();
        //如下調整水位標記
        start = new_start;
        finish = new_finish;
        end_of_storage = new_start + len;
        }
    }
}複製代碼

插入點、備用空間大小、待插入元素數量等角度進行分類:

1-1 插入點以後元素個數大於新增元素個數:

A. 將finish以前的n個元素移到finish以後
B. finish變動爲finish + n獲得newfinish
C. 將position以後到oldfinish - n之間的元素後移到oldfinish位置
D. 從position開始填充n個x

1-2 插入點以後元素小於新增元素個數

A. 將差值補到finish以後
B. finish變動爲finish + n - elems_after獲得newfinish
C. 將position到oldfinish的元素拷貝到newfinish以後
D. 在position到oldfinish之間插入x

                                摘自 stl源碼剖析
                               摘自 stl源碼剖析

2. 備用空間小於新增元素個數

A. 申請空間,old_size + max(old_size, n)
B. 複製填充:
c. 將原vector插入點以前的元素拷貝到新空間
D. 將新增元素填入新空間
E. 將原vector插入點以後的元素拷貝到新空間

                                                  摘自 stl源碼剖析

這個過程仍是比較有意思的,其中涉及到了幾個泛型拷貝:

uninitialized_copy和copy_backward等,能夠再本身細緻地畫一下圖理解這個過程,在此再也不贅述。

參考資料

相關文章
相關標籤/搜索