STL之vector的push_back過程詳解
阿新 • • 發佈:2019-02-11
最近,被面試官的一道題問倒,很失落,明明看過《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);
就寫到這裡吧,繼續面壁思過了。