1. 程式人生 > >必須要注意的 C++ 動態記憶體資源管理(六)——vector的簡單實現

必須要注意的 C++ 動態記憶體資源管理(六)——vector的簡單實現

十六.myVector分析

        我們知道,vector類將其元素存放在連續的記憶體中。為了獲得可接受的效能,vetor預先分配足夠大的記憶體來儲存可能需要的更多元素。vector的每個新增元素的成員函式會檢查是否有空間容納更多的元素。如果有,成員函式會在下一個可用位置構造一個物件。如果沒有可用空間,vector就會重新分配空間;它獲得新的空間,將已有元素移動到新空間中,釋放舊空間,並新增新元素。         既然是動態開闢的記憶體,於是我們在myVector中使用動態陣列來儲存,而每次插入元素的時候需要先判斷開闢的記憶體是否已滿,如果滿了需要重新分配記憶體。
        下面給出最初版本的程式碼:
//myVector.h
#include <memory>

template<typename T>
class myVector
{
public:
    typedef myVector<T> _Myt;
    myVector() :
        elements(nullptr), first_free(nullptr), cap(nullptr){}    // allocator成員進行預設初始化
    myVector(const _Myt&);
    _Myt& operator=(const _Myt&);
    ~myVector();
    T& operator
[](size_t i){ return *(elements + i); } void push_back(const T&); // 新增元素 size_t size()const{ return first_free - elements; } size_t capacity()const{ return cap - elements; } T *begin()const{ return elements; } T *end()const{ return first_free; } private
: void chk_n() //被新增元素函式使用 { if (size() == capacity())reallocate(); } std::pair<T*, T*> n_copy (const T*, const T*); //被拷貝構造,賦值運算子,解構函式使用 void free(); //銷燬元素並釋放記憶體 void reallocate(); //獲得更多記憶體並拷貝已有元素 T *elements; T *first_free; T *cap; }; template<typename T> myVector<T>::myVector(const _Myt& v) { //呼叫alloc_n_copy 分配空間以容納與s中一樣多的元素 auto newdata = n_copy(v.begin(), v.end()); elements = newdata.first; first_free = cap = newdata.second; } template<typename T> myVector<T>::~myVector() { free(); } template<typename T> myVector<T>& myVector<T>::operator=(const _Myt& rhs) { //呼叫alloc_n_copy分配記憶體,大小與rhs一樣. auto data = n_copy(rhs.begin(), rhs.end()); free(); elements = data.first; first_free = cap = data.second; return *this; } template<typename T> std::pair<T*, T*> myVector<T>:: n_copy(const T *b, const T *e) { auto data = new T[e - b]; for (auto i = b; i < e; i++) data[i-b] = *i; return{ data, data + (e - b) }; } template<typename T> void myVector<T>::push_back(const T& s) { chk_n(); //確保已有新空間 *(first_free++) = s; } template<typename T> void myVector<T>::free() { delete[] elements; } template<typename T> void myVector<T>::reallocate() { //我們將分配當前大小兩倍的記憶體空間 auto newcapacity = size() ? 2 * size() : 1; //分配新記憶體 auto newdata = new T[newcapacity]; auto dest = newdata; auto elem = elements; //將資料從舊地址移動到新地址 for (size_t i = 0; i != size(); ++i) *(dest++) = *(elem++); free(); //一旦更新完就要釋放舊記憶體 elements = newdata; first_free = dest; cap = elements + newcapacity; }
        恩,以上程式碼實現了vector的部分功能,實現了vector記憶體的動態分配。不過,我們可以發現以上程式碼還是有幾個可以優化的地方:
  • 1.在分配記憶體的時候,new將記憶體分配和物件構造組合在了一起。但是在vector分配記憶體的時候,事實上有許多記憶體我們可能並用不上;而如果對於這些記憶體進行構造物件,可能就會帶來不必要的開銷。
  • 2.在記憶體重新分配的時候,我們涉及到了舊資料的轉移;不對,這裡應該說是拷貝。雖然的確應該是轉移,然而我們實現是通過拷貝。在c++11的時候提供了移動構造語義。它可以對於即將銷燬(保證重新賦值前不再使用)的物件進行移動。這樣對於某些支援移動語義的物件。移動比拷貝就可以帶來更小的開銷。
        恩,要進行以上的優化我們要先介紹:allocator類,移動語義。

十七.allocator類介紹

        new有一些靈活性的侷限,一方面表現在它將記憶體分配和物件構造組合在一起。類似的,delete將物件析構和記憶體釋放組合在一起。我們分配單個物件時,通常我們希望將記憶體和物件初始化放在一起。因為在這樣的情況下,我們幾乎已經知道了物件應當是什麼值。
        當分配一大塊記憶體時,我們通常計劃在這塊記憶體上按需構造物件。在這種情況下,我們就應該希望將記憶體分配和物件構造分離。這意味著我們可以分配大塊記憶體,然而只有我們真正需要的時候才執行物件建立操作(同時付出一定開銷)。
        標準庫allocator類定義在標頭檔案memory中,它幫助我們將記憶體分配和物件構造分離開來。
函式 介紹
allocator<T> a 定義了一個名為a的allocator物件,它可以為型別為T的物件分配記憶體。
a.allocate(n) 分配一段原始的,未構造的記憶體,儲存n個型別為T的物件。
a.deallocate(p,n) 釋放從T*指標p開始的記憶體,這塊記憶體儲存了n個型別為T的物件;p必須是一個先前由allocate返回的指標,且n必須是p建立時指定的大小。在呼叫deallocate之前,使用者必須對每個在這塊記憶體建立的記憶體呼叫destroy。
a.construct(p,args) p必須是一個型別為T*的指標,指向一塊原始記憶體;args被傳遞給型別為T的建構函式,用來在p指向的記憶體上構造一個物件。
a.destroy(p) p為T*型別指標,此演算法對p指向的物件執行解構函式。
        下面是一段簡單的程式碼,介紹瞭如何使用allocator進行記憶體分配,物件構造,物件釋放,記憶體回收。
int main()
{
    allocator<string>  alloc;          //這個物件可以用來分配 string 型別的記憶體。
    string* p = alloc.allocate(5);     //使用alloc物件分配5個string物件大的連續記憶體並將頭指標給p。

    //allocate分配的記憶體在沒有構造之前是不能使用的!!
    alloc.construct(p,"hello world");   //使用"hello world"構造string

    cout << *p << endl;       //輸出剛才構造的string,輸出hello world

    alloc.destroy(p);         //銷燬剛才構造的物件

    alloc.deallocate(p,5);    //釋放記憶體
    return 0;
}
        不僅這樣,標準庫還提供了一些演算法讓我們使用的時候更加方便: |函式|介紹| |—|—-| |uninitialized_copy(b,e,b2)|從迭代器b和e指出的輸入範圍中拷貝元素到迭代器b2指定的未構造的原始記憶體中。b2指向的記憶體必須足夠大,能容納輸入序列中元素的拷貝。 |uninitialized_copy_n(b,n,b2)|從迭代器b指向的元素開始,拷貝n個元素到b2開始的記憶體中。 |uninitialized_fill(b,e,t)|在迭代器b,e指定的原始記憶體範圍中建立物件,物件的值均為t的拷貝。 |uninitialized_fill_n(b,n,t)|從迭代器b指向的記憶體地址開始建立n個物件。b必須指向足夠大的未構造原始記憶體,能夠容納給定數量的物件。
        值得注意的是,所有通過allocate分配的記憶體都必須通過deallocate去回收,而所有構造的物件都必須通過destroy去釋放。所以這些拷貝演算法都必須要求原始記憶體,如果記憶體上有物件,請先使用destroy釋放!!

十八.移動語義介紹

        很多情況下都會發生物件拷貝,然而在其中某些情況下,物件拷貝後就會立即被銷燬。在這些情況下,移動而非拷貝物件會大幅度提升效能。還有的情況諸如IO類或者unique_ptr這些類都包含不能共享的資源,因此這些類的物件也不能拷貝只能移動。         為了支援移動操作,在新標準中引入了一種新的引用型別 —— 右值引用。右值引用有個重要的性質:只能繫結到一個即將要銷燬的物件上。因此我們可以自由地將一個右值引用的資源”移動”到另一個物件中。下面給出一些例子來表示哪些是右值:
int i = 42;                 
int &r = i;                   //正確:r引用i
int &&rr = i;                 //錯誤:不能將一個右值引用繫結到左值上
int &r2 = i * 42;             //錯誤:i * 42 是個右值
const int & r3 = i * 432;     //正確:我們可以把一個const引用繫結到右值上
int &&rr2 = i * 42;           //正確:右值引用繫結右值
        考察左值和右值的區別:左值有持久的狀態,而右值要麼是字面常量,要麼是在表示式求值過程中或者是函式返回的時候建立的臨時變數。
        因為變數是左值,所以我們不能將一個右值引用繫結到一個變數上,即使這個變數是一個右值引用型別也不行。所以為了解決這個問題,標準庫提供了一個move函式,它可以用來獲得繫結到左值上的右值引用。此函式定義在標頭檔案utility中。
        我們可以銷燬一個移後源物件,也可以賦予新值,但不能在賦新值之前使用移後源物件的值。
        根據以上所說,如果類也支援移動拷貝和移動賦值,那麼也能在某些時候的初始化(賦值)的時候提高效能。如果要想讓類支援移動語義,我們需要為其定義移動建構函式和移動賦值運算子。這兩個函式的引數都是一個右值引用。就如同上面的程式碼,對於vector的移動我們只需要拷貝三個指標引數,而不是拷貝三個指標引數指向的值。
template<typename T>
myVector<T>::myVector(_Myt&& v):elements(v.elements),first_free(v.first_free),cap(v.cap){
    v.elements = v.first_free = v.cap = nullptr;
}
        值得注意的是,我們要保證移後源物件必須是可析構狀態,而且如果移動構造(賦值)函式不丟擲異常的話必須要標記為noexcept(primer p474)。         對於移動賦值運算子我們要保證能正確處理自我賦值:
template<typename T>
myVector<T>& myVector<T>::operator=(_Myt&& rhs)
{
    if (this != &rhs)
    {
        free();
        elements = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        //將rhs置為可析構狀態
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
}
        當然,和其他建構函式一樣,如果我們沒有定義移動建構函式的時候,編譯器會給我們提供預設的移動建構函式。不過,前提是該類沒有定義任何版本的拷貝控制函式以及每個非staitc成員變數都可以移動。編譯器就會預設為它合成移動建構函式和移動賦值運算子。

十九.優化過後的Vector

        我們使用 allocate 和移動語義對以上的vector進行優化:
#pragma once

#include <memory>

template<typename T>
class myVector
{
public:
    typedef myVector<T> _Myt;
    myVector() :
        elements(nullptr), first_free(nullptr), cap(nullptr){}    // allocator成員進行預設初始化
    myVector(const _Myt&);
    myVector(_Myt&&);
    _Myt& operator=(const _Myt&);
    _Myt& operator=(_Myt&&);
    ~myVector();
    T& operator[](size_t i){ return *(elements + i); }
    void push_back(const T&);                                      // 新增元素
    size_t size()const{ return first_free - elements; }
    size_t capacity()const{ return cap - elements; }
    T *begin()const{ return elements; }
    T *end()const{ return first_free; }
private:
    static std::allocator<T> alloc;
    void chk_n_alloc()                           //被新增元素函式使用
    {
        if (size() == capacity())reallocate();
    }
    std::pair<T*, T*> alloc_n_copy
        (const T*, const T*);          //被拷貝構造,賦值運算子,解構函式使用
    void free();                       //銷燬元素並釋放記憶體
    void reallocate();                 //獲得更多記憶體並拷貝已有元素

    T *elements;
    T *first_free;
    T *cap;
};

template<typename T>
std::allocator<T> myVector<T>::alloc;

template<typename T>
myVector<T>::myVector(const _Myt& v)
{
    //呼叫alloc_n_copy 分配空間以容納與s中一樣多的元素
    auto newdata = alloc_n_copy(v.begin(), v.end());
    elements = newdata.first;
    first_free = cap = newdata.second;
}
template<typename T>
myVector<T>::myVector(_Myt&& v):elements(v.elements),first_free(v.first_free),cap(v.cap){
    v.elements = v.first_free = v.cap = nullptr;
}

template<typename T>
myVector<T>::~myVector()
{
    free();
}
template<typename T>
myVector<T>& myVector<T>::operator=(const _Myt& rhs)
{
    //呼叫alloc_n_copy分配記憶體,大小與rhs一樣.
    auto data = alloc_n_copy(rhs.begin(), rhs.end());

    free();

    elements = data.first;
    first_free = cap = data.second;

    return *this;
}
template<typename T>
myVector<T>& myVector<T>::operator=(_Myt&& rhs)
{
    if (this != &rhs)
    {
        free();
        elements = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        //將rhs置為可析構狀態
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
}

template<typename T>
std::pair<T*, T*> myVector<T>::
alloc_n_copy(const T *b, const T *e)
{
    auto data = alloc.allocate(e - b);

    //初始化並返回一個pair,該pair由data和uninitialized_copy組成
    return{ data, uninitialized_copy(b, e, data) };
}
template<typename T>
void myVector<T>::push_back(const T& s)
{
    chk_n_alloc();              //確保已有新空間
    alloc.construct(first_free++, s);
}

template<typename T>
void myVector<T>::free()
{
    //不能傳遞給deallocate一個空指標,如果elements為NULL,那麼函式什麼都不做
    if (elements)
    {
        //逆序銷燬所有元素
        for (auto p = first_free; p != elements;/* 空 */)
            alloc.destroy(--p);
        alloc.deallocate(elements, cap - elements);
    }
}

template<typename T>
void myVector<T>::reallocate()
{
    //我們將分配當前大小兩倍的記憶體空間
    auto newcapacity = size() ? 2 * size() : 1;

    //分配新記憶體
    auto newdata = alloc.allocate(newcapacity);

    //將資料從舊地址移動到新地址
    auto dest = newdata;
    auto elem = elements;

    for (size_t i = 0; i != size(); ++i)
        alloc.construct(dest++, std::move(*elem++));
    free();  //一旦更新完就要釋放舊記憶體

    elements = newdata;
    first_free = dest;

    cap = elements + newcapacity;
}
        儘管,以上的程式碼vector只實現了vector很少的一部分功能,而且可能實現方式也有不足的地方。不過,在這裡只是想體現動態記憶體的使用。所以,以上的程式碼還是可以作為c++動態記憶體管理的的示例的。
基本上c++動態記憶體管理的就介紹到這裡了。