天天看點

從零開始學C++之STL(二):實作簡單容器模闆類Vec(vector capacity 增長問題、allocator 記憶體配置設定器)

首先,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,如下圖所示:

從零開始學C++之STL(二):實作簡單容器模闆類Vec(vector capacity 增長問題、allocator 記憶體配置設定器)

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 的工作原理,寫個小程式測試一下:

從零開始學C++之STL(二):實作簡單容器模闆類Vec(vector capacity 增長問題、allocator 記憶體配置設定器)

從輸出可以看出,構造函數調用3次,拷貝構造函數調用6次,析構函數調用9次,下面來分析一下,首先看下圖:

從零開始學C++之STL(二):實作簡單容器模闆類Vec(vector capacity 增長問題、allocator 記憶體配置設定器)

首先定義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; 來測試的話,輸出略有不同,如下:

從零開始學C++之STL(二):實作簡單容器模闆類Vec(vector capacity 增長問題、allocator 記憶體配置設定器)

輸出的次數是一緻的,隻是拷貝的順序有所不同而已,比如第二次調用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++