STL vector
一.vector的定義與結構
vector的資料結構是一個簡單的線性連續空間,其中有三個迭代器分別指向連續空間中已用空間的起始和結尾以及連續空間的結尾,它們是:_M_start,_M_finish,_M_end_of_storage。vector的定義摘要部分如下:
template<class _Tp, class _Alloc> class _Vector_base{ public: typedef _Alloc allocator_type; // 返回所使用配置器型別的一個物件 allocator_type get_allocator() const { return allocator_type(); } _Vector_base(const _Alloc&) : _M_start(0), _M_finish(0), _M_end_of_storage(0) {} _Vector_base(size_t __n, const _Alloc&) : _M_start(0), _M_finish(0), _M_end_of_storage(0) { _M_start = _M_allocate(__n); // 呼叫配置器分配空間 _M_finish = _M_start; _M_end_of_storage = _M_start + __n; // 指向容器尾部 } protected: _Tp* _M_start; // 已用空間首部迭代器 _Tp* _M_finish; // 已用空間尾部迭代器 _Tp* _M_end_of_storage; // 申請空間尾部迭代器 typedef simple_alloc<_Tp, _Alloc> _M_data_allocator; _Tp* _M_allocate(size_t n) { return _M_data_allocator::allocte(n); //simple_alloc介面會轉而呼叫一級或二級配置器,並返回首地址 } void _M_deallocate(_Tp* __p, size_t __n) { _M_data_allocator::deallocate(__p, __n); } }; //【注】:由SGI STL 中的定義: // - typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; // - define __STL_DEFAULT_ALLOCATOR(T) alloc // 可知預設使用第二級配置器(當小於128位元組時第二級會呼叫一級配置器) template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) > class vector : protected _Vector_base<_Tp, _Alloc> { private: typedef typename _Base::allocator_type allocator_type; ... public: typedef value_type* iterator; // 反向迭代器,定義於stl_iterator.h中 typedef reverse_iterator<iterator, value_type, reference, difference_type> reverse_iterator; vector(const allocator_type& __a = allocator_type()) : _Vector_base<_Tp, _Alloc>(__a) {} iterator begin() { return _M_start; } // - 用_Tp*初始化反向迭代器 // - rend()返回的反向迭代器指向*_M_start reverse_iterator rend() { return reverse_iterator(begin()); } ... };
二.vector的記憶體管理
要說明vector的記憶體管理,就先從vector的記憶體構造開始說起吧。以下是vector中的一個建構函式,接受一個迭代器所指的區間作為引數:
vector(const _Tp* __first, const _Tp* __last, const allocator_type& __a = allocator_type()) : _Vector_base<_Tp, _Alloc>(__last - __first, __a) { _M_finish = uninitialized_copy(__first, __last, _M_start); }
在構造時vector首先在基類_Vector_base的建構函式中呼叫了空間配置器申請了一塊大小為_last - _first的記憶體空間,注意這塊空間是未初始化的,接下來將[_first, _last)區間內的物件呼叫uninitialized_copy函式(拷貝呼叫參:《STL 空間配置器(三)》)拷貝到vector的記憶體空間中。
接下來再思考當插入資料時vector是如何處理記憶體不夠問題的,即如何進行記憶體擴充的,我們可以通過觀察push_back函式的實現來窺得這一點。
void push_back(const _Tp& __x) { if (_M_finish != _M_end_of_storage) { // 如果還有剩餘記憶體空間,則在_M_finish位置上構造物件 construct(_M_finish, __x); ++_M_finish; } else _M_insert_aux(end(), __x); // 否則呼叫_M_insert_aux函式 } template <class _Tp, class _Alloc> void vector<_Tp, _Alloc>::_M_insert_aux(iterator __position, const _Tp& __x) { if(_M_finish != _M_end_of_storage) { // 進行元素後移 // 想想為什麼要進行這一步,二不是直接後移 construct(_M_finish, *(_M_finish - 1)); ++_M_finish; _Tp __x_copy = __x; // 將區間[position, _M_finish - 2)的內容反向拷貝到以_M_finish - 1結尾的區間 copy_backword(position, _M_finish - 2, _M_finish - 1); *__position = x; } else { // 已無備用空間 const size_type __old_size = size(); const size_type __len = __old_size != 0 ? 2 * __old_size : 1; iterator __new_start = _M_allocate(__len); iterator __new_finish = __new_start; try{ // 分兩步:從_M_start到__position, 從 ___position 到 _M_finish進行拷貝到新記憶體 __new_finish = uninitialized_copy(_M_start, __position, __new_start); construct(__new_finish, __x); ++__new_finish; __new_finish = uninitialized_copy(__position, _M_finish, __new_finish); } catch(...){ // 若拷貝過程出現錯誤,則析構掉新記憶體中已構造的物件,並回收新記憶體區。 destroy(__new_start,__new_finish), _M_deallocate(__new_start,__len)) } destroy(begin(), end()); //析構舊記憶體區中的物件 _M_deallocate(_M_start, _M_end_of_storage - _M_start);//回收舊記憶體區 _M_start = __new_start; _M_finish = __new_finish; _M_end_of_storage = __new_start + __len; } }
從以上原始碼可看出,當舊的記憶體區間不足時,會嘗試申請一個是舊區間2被大小的新記憶體區(當然可能比這個小,原因請參照《STL 空間配置器(三)》。然後將舊記憶體區中的資料拷貝到新記憶體區(如果物件不是POD型別,那麼此過程會呼叫拷貝建構函式,否則呼叫memmove),由此點,我們程式設計時對於那些不需要自己實現建構函式的class型別不應該畫蛇添足的新增一個建構函式,因為這回影響STL容器以及一些演算法的執行效率的執行效率。
還有一點是在程式碼中提出的:當前空間足夠時為什麼要先呼叫construct(_M_finish, __x),而不是直接使用copy_backward後移?這是因為_M_finish所指的那塊區間是還未進行過初始化的,若直接使用copy_backward,那麼對於POD型別或是隻有預設賦值運算子的POD型別而言來說沒有太大問題,因為會直接呼叫memmove,而對於有非預設賦值運算子的非POD型別而言(即是非POD型別,且有非預設賦值運算子),會執行行*result = *first這樣的語句,其中result指向的是還未經初始化的記憶體區間,而呼叫其賦值運算子就更加是一個未定義的事件了。
三.vector的元素操作
關於vector的元素操作沒有太多好說的,但是insert函式的實現中有值得商討的地方。
insert函式的處理方式如下:
1)若vector中的剩餘大小足夠存放插入的元素則分為以下兩種情況進行處理
if (插入點之後的元素個數:after_element > 要插入的元素個數:n) {
總共要移動的個數是插入點之後的元素個數;
而對要移動的這部分元素分為兩部分進行移動;
第一部分:靠後的n個元素要被移動到未被初始化過的記憶體區,因此呼叫uninitialized_copy()函式來移動這n個元素。
第二部分:插入點之後的 after_element - n個元素也需要向後移動n個位置,可是這n個位置是已被初始化過的記憶體區。因此STL採用
了copy_backward()來進行拷貝,這樣做的原因是使用uninitialized_copy函式來操作以被初始化過的記憶體區間會造成建構函式
與解構函式的呼叫
次數不相等,這樣可能會造成記憶體洩露等問題。
最後使用引數x來填充插入點後的n個元素。
}
else{//插入點之後的元素個數:after_element <= 要插入的元素個數:n
新元素若插入後在記憶體區間結尾應該是_M_finish + n - after_element;
因此先呼叫uninitialized_fill函式_M_finish處填充n - after_element個新元素。
再呼叫uninitialized_copy將插入節點後的after_element箇舊資料移到_M_finish + n - after_element之後
【注意】由於上述操作的目的區間都是未初始化的,因此只能呼叫uninitialized_xxx函式
最後一一步是填充已被拷貝過的在插入節點後的after_element個記憶體區,由於這塊區間是已被初始化過的,因此只能呼叫fill
}
2)若備用空間不足,則進行擴充並拷貝和填充,過程相似於前面提到的_M_insert_aux函式,此處不再贅述