首先,vector 在VC 2008 中的實作比較複雜,雖然vector 的聲明跟VC6.0 是一緻的,如下:
但在VC2008 中vector 還有基類,如下:
稍微來看一下基類_Vector_val:
為了了解_Alty 的類型,還得看一下allocator模闆類:
typedef typename _Alloc::template rebind<_Ty>::other _Alty; 整體來看是類型定義,假設現在我們這樣使用
vector<int>, 那麼_Ty 即 int, _Ax 即 allocator<int>,由vector 類傳遞給 基類_Vector_val,則_Alloc 即
allocator<int> ;可以看到 allocator<void> 是allocator 模闆類的特化, rebind<_Ty> 是成員模闆類,other是成員模闆類
中自定義類型,_Ty 即是int , 那麼other 類型也就是allocator<int>, 也就是說_Alty 是類型 allocator<int> 。
_Alty _Alval; 即 基類定義了一個allocator<int> 類型的成員,被vector 繼承後以後用于為vector 裡面元素配置設定記憶體等操作。
如 iterator new_data = alloc.allocate(new_size); 注意,标準的vector::iterator 是以模闆類實作的,下面的實作簡單地将其等同為指
針,實際上真正的iterator 類的實作是内部有一個指針成員,指向容器元素。
對比list 的實作,繼承它的基類_List_nod 的一個成員 allocator<_Node> _Alnod; 如下:
typename _Alloc::template rebind<_Node>::other _Alnod; // allocator object for nodes
其中 _Node有三個成員,如下:
_Nodeptr _Next; // successor node, or first element if head
_Nodeptr _Prev; // predecessor node, or last element if head
_Ty _Myval; // the stored value, unused if head
如果是list<int> ,那麼_Ty 即是int 類型。雙向連結清單在建立一個新結點時,這樣實作:
_Nodeptr _Pnode = this->_Alnod.allocate(1); // 即配置設定一個節點的空間,傳回指向這個節點的指針。
實際上list 還繼承另外兩個基類的兩個成員,如下:
typename _Alloc::template rebind<_Nodeptr>::other _Alptr;// allocator object for pointers to nodes
typename _Alloc::template rebind<_Ty>::other _Alty _Alval; // allocator object for values stored in nodes
在VC6.0 vector 的實作,_Alval 是直接作為vector 自身的成員存在的。此外還有一個比較大的不同點在于,兩個版本對于capacity 也就是容量的
計算方式不同,接下去的測試可以看到這種不同,在這裡可以先說明一下:
VC2008:容量每次增長為原先容量 + 原先容量 / 2;
VC6.0 :容量每次增長為原先容量的2倍。
容量跟vector 大小的概念是不一樣的,capacity 》= size,如下圖所示:

size 指的是avail - data 的區間;capacity 指的是 limit - data 的區間;也就是說存在尚未使用的空間。
data, avail 和 limit 是vector 類的三個指針成員,對比list 的實作,自身也許隻有兩個成員,如下:
Nodeptr _Myhead; // pointer to head node
size_type _Mysize; // number of elements
實際上 list不僅是一個雙向連結清單,而且還是一個環狀雙向連結清單。另外,插入操作和結合操作都不會造成原有的list疊代器失效,這在vector是不成立的。
因為vector的插入操作可能造成存儲空間重新配置,導緻原有的疊代器全部失效。list的元素删除操作(erase),也隻有“指向被删除元素”的那個
疊代器失效,其他疊代器不受任何影響。
下面是模仿VC6.0 中vector 的實作寫的Vec 類,程式主要參考《Accelerated C++》 ,略有修改,比如将接口修改成與VC6.0 一緻,
這樣做的好處是可以傳遞第二個參數,也就是說可以自己決定記憶體的配置設定管理方式;實作capacity() 函數等;
Vec.h:
先介紹一下用到的一些類和函數:
allocator 模闆類:
當然實際的接口沒實作沒那麼簡單,但大概實作的功能差不多:
allocate 調用operator new ;deallocate 調用 operator delete; construct 調用placement new (即在配置設定好的内
存上調用拷貝構造函數),destroy 調用析構函數。
兩個std函數:
如 std::uninitialized_copy(i, j, data); 即将i ~ j 指向區間的數值都拷貝到data 指向的區間,傳回的是最後一個初始化值的下一個位置。
std::uninitialized_fill(data, limit, val); 即将 data ~ limit 指向的區間都初始化為val 。
為了了解push_back 的工作原理,寫個小程式測試一下:
從輸出可以看出,構造函數調用3次,拷貝構造函數調用6次,析構函數調用9次,下面來分析一下,首先看下圖:
首先定義t1, t2, t3的時候調用三次構造函數,這個沒什麼好說的;接着第一次調用push_back,調用grow進而調用alloc.allocate,
allocate 函數調用operator new 配置設定一塊記憶體,第一次uncreate 沒有效果,接着push_back 裡面調用uncheck_append,進而調用
alloc.construct,即調用placement new(new (_Vptr) _T1(_Val); ),在原先配置設定好的記憶體上調用一次拷貝構造函數。
接着第二次調用push_back,一樣的流程,這次先配置設定兩塊記憶體,将t1 拷貝到第一個位置,調用uncreate(),先調用alloc.destroy,即
調用一次析構函數,接着調用alloc.deallocate,即調用operator delete 釋放記憶體,最後調用uncheck_append将t2 拷貝到第二個位置。
第三次調用push_back,也一樣配置設定三塊記憶體,将t1, t2 拷貝下來,然後分别析構,最後将t3 拷貝上去。
程式結束包括定義的三個Test 對象t1, t2, t3 ,析構3次,Vec<Test> v2; v2是局部對象,生存期到則調用析構函數~Vec(); 裡面調用
uncreate(), 調用3次Test 對象的析構函數,調用operator delete 釋放3個對象的記憶體。故總共析構了6次。
在VC2008 中換成 vector<Test> v2; 來測試的話,輸出略有不同,如下:
輸出的次數是一緻的,隻是拷貝的順序有所不同而已,比如第二次調用push_back 的時候,VC2008 中的vector 是先拷貝t2, 接着拷
貝t1, 然後将t1 釋放掉。
最後再來提一下關于capacity 的計算,如下的測試程式:
輸出為 1 2 4 4 8 8 8 即在不夠的情況下每次增長為原來的2倍。
如果換成 vs2008 下 vector<int> v; 測試,那麼輸出是 1 2 3 4 6 6 9,即在不夠的情況每次增長為原來大小 + 原來大小 / 2;
看到這裡,有些朋友會疑惑了,由1怎麼會增長到2呢?按照原則不是還是1?其實隻要看一下vector 的源碼就清楚了:
_Insert_n 是被push_back 調用的,當我們試圖增長為_Capacity + _Capacity / 2; 時,下面還有一個判斷:
if (_Capacity < size() + _Count)
_Capacity = size() + _Count;
現在試圖增長為 1 + 1/ 2 = 1; 此時因為 1 < 1 + 1 ; 是以 _Capacity = 1 + 1 = 2;
其他情況都是直接按公式增長。
從上面的分析也可以看出,當push_back 的時候往往帶有拷貝和析構多個操作,是以一下子配置設定比size() 大的空間capacity,可以減輕頻繁操作造成的
效率問題。
參考:
C++ primer 第四版
Effective C++ 3rd
C++程式設計規範
Accelerated C++