1. 程式人生 > >STL之vector的push_back過程詳解

STL之vector的push_back過程詳解

最近,被面試官的一道題問倒,很失落,明明看過《STL原始碼分析》,為啥這種問題還沒答好,只能說自己看的時候沒有仔細去思考。這道題就是標題的問題,面試完我重新看了一遍《STL原始碼分析》中關於這塊的內容,這裡記錄下自己看完的一點理解。

在STL中,一般對容器的記憶體分配和構造是分開的2個過程,STL有專門的空間配置器負責分配記憶體,而構造則是通過placement new在已申請的記憶體上進行的,vector也不除外,下面是vector的push_back函式原始碼:

template <class T, class Alloc = alloc>
void vector::push_back(const T& x)
{
    if (finish != end_of_storage)
    {
        construct(finish, x);
        ++finish;
    }
    else
    {
        insert_aux(finish, x);
    }
}

其中,construct是STL的全域性函式,是所有容器共用的,它的具體實現如下:

template<class T1, class T2>
inline void construct(T1* p, const T2& value)
{
    new(p) T1(value);    //placement new, 呼叫T1::T1(value);
}

而insert_aux是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 copy_x = x;
        //把vector插入位置position之後的元素往後移一位
        copy_backward(position, finish - 2, finish - 1);
        //給position指向的地方賦值,賦值內容為前面拷貝的元素
        *position = copy_x;
    }
    //已無備用空間
    else
    {
        const size_type old_size = size();
        //如果原來的vector為空,則申請一個元素的空間,否則申請可以容納原來2倍元素的空間
        const size_type len = old_size == 0 ? 1 : 2 * old_size;
        //全域性函式,申請空間
        iterator new_start = data_allocator::allocate(len);
        iterator new_finish = new_start;

        try
        {
            //將原來vector的position之前的內容拷貝到新的vector前面
            new_finish = uninitialized_copy(start, position, new_start);
            //呼叫建構函式為新插入的元素賦值
            construct(new_finish, x);
            //調整水位
            ++new_finish;
            //將原來vector的postion之後的內容拷貝到新的vector後面
            new_finish = uninitialized_copy(position, finish, new_finish);
        }
        catch (...)
        {
            //析構
            destroy(new_start, new_finish);
            //釋放剛剛申請的記憶體
            data_allocator::deallocate(new_start, len);
            throw;
        }

        //析構原vecotr
        destroy(begin(), end());
        //釋放原vector的空間
        deallocate();
        
        //調整迭代器指向新的vector
        start = new_start;
        finish = new_finish;
        end_of_storage = new_start +len;
    }
}

上面空間配置函式allocate的最底層實現比較複雜,但是隻要記住2點即可,(1)如果申請的空間大於128位元組,就直接呼叫malloc申請,(2)如果申請的空間小於等於128位元組,就從STL維護的16條free list裡面尋找合適的一塊記憶體使用(這16條free list各自管理大小分別為8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128位元組的小額記憶體塊,之所謂維護這些list既是為了防止頻繁呼叫malloc,也是為了避免太多小額區塊造成記憶體的碎片)。

介紹了這麼多,回到最初的問題,如果我們一開始定義了一個空的vector,然後現在要往裡面push_back一個個物件,這個過程具體是怎樣的,請看下面這段程式碼的註釋,這些都是我重新看了一遍書的理解:

//定義一個類A
class A{
    int x;
    double y;
};

//定義一個空的vector,vector中可以存放的是類A的物件
vector<A> vec;

//定義類A的物件a,物件b,物件c和物件d
A a;
A b;
A c;
A d;

/*
注意:根據上面對push_back的原始碼分析可知,
因為一開始vec是空的,所以會走insert_aux函式的無可用空間的分支,
呼叫allocate申請一塊能夠容納《一》個A類物件的記憶體,
並呼叫拷貝建構函式把a賦值給vec的finish迭代器指向的記憶體,
說白了就是vec中存放的a和上面定義的a物件已經不是一個東西了。
*/
vec.push_back(a);

/*
注意:根據上面對push_back的原始碼分析可知,
現在vec也沒有多餘的可用空間,所以會再次走insert_aux函式的無可用空間的分支,
呼叫allocate申請一塊能夠容納《兩》個A類物件的記憶體,
把原來的vec的唯一一個元素a移動到新的vec上去,
並呼叫拷貝建構函式把b賦值給新的vec的finish迭代器指向的記憶體,這時a和b存放在相鄰記憶體中。
*/
vec.push_back(b);

/*
注意:根據上面對push_back的原始碼分析可知,
現在vec也沒有多餘的可用空間,所以會再次走insert_aux函式的無可用空間的分支,
呼叫allocate申請一塊能夠容納《四》個A類物件的記憶體,
把原來的vec的兩個元素a和b移動到新的vec上去,
並呼叫拷貝建構函式把c賦值給新的vec的finish迭代器指向的記憶體,
這時a和b和c存放在相鄰記憶體中,而且這塊記憶體還能再容納一個類A物件。
*/
vec.push_back(c);

/*注意:根據上面對push_back的原始碼分析可知,
現在vec還有一個可用空間,所以這次會走construct函式的分支,
通過placement new在已有的記憶體上呼叫拷貝建構函式把d賦值給finish迭代器指向的記憶體,
這時a和b和c和d存放在相鄰記憶體中,但這塊記憶體又再次沒有剩餘空間了。
*/
vec.push_back(d);

就寫到這裡吧,繼續面壁思過了。