C++記憶體分配與物件構造的分離

QingLiXueShi發表於2015-04-09

在C++中,我們基本用new(delete)操作符分配(釋放)記憶體。new操作符為特定型別分配記憶體,並在新分配的記憶體中構造該型別的一個物件。new表示式自動執行合適的建構函式來初始化每個動態分配的類型別物件。即new表示式既分配了記憶體同時也構造了物件。

然而,我們一定會遇到這樣的情況:預先分配用於建立新物件的記憶體,需要時在預先分配的記憶體中構造每個物件。即將記憶體分配與物件構造分開進行,這樣做的理由是:

(1)在記憶體分配時構造物件很浪費,可能會建立從不使用的物件。

(2)當實際使用預先分配的物件時,被使用的物件很可能要重賦新值。

string* pstr = new string[5]; 

上面舉了個不合適的例子(當然你應該用vector<string>來代替),毫無疑問被分配的5個string空間是被string預設建構函式初始化了,而且接下來你肯定得對pstr[0...4]重新賦值。所以new操作符這種分配特點會增加執行時開銷。尤其是某些使用者的類型別要求物件分配更快一些,做法通常是:預先分配用於建立新物件的記憶體,需要時在預先分配的記憶體中構造每個新物件。

 

一、分配原始記憶體

C++提供兩種方法分配和釋放未構造的原始記憶體:

(1)allocator類,它提供可感知型別的記憶體分配。這個類支援抽象介面,以分配記憶體並隨後使用該記憶體儲存物件。

2)標準庫中的operator new和operator delete,它們分配和釋放需要大小的原始的,未型別化的記憶體。

1、allocator類

allocator類是一個模板,它提供型別化的記憶體分配以及物件構造與撤銷。它支援的操作如下:

allocator類將記憶體分配和物件構造分開。當allocator物件分配記憶體的時,它分配適當大小並排列成儲存給定型別物件的空間。它分配的記憶體是未被構造的,allocator的使用者必須分別construct和destroy放置在該記憶體中的物件。

 

vector的自增長告訴我們:vector為了實現快速記憶體分配,其實際分配的空間要比當前需要的空間多一些。(實際空間因庫的實現不同而不同),下面為了說明allocator的使用,我們簡陋地實現STL vector中的push_back操作。

template <class T> class VECTOR
{
public:
    VECTOR() : elements(NULL), first_free(NULL), end(NULL){}
    void push_back(const T&);
private:
    static allocator<T> alloc;
    void reallocate();
    T *elements;
    T *first_free;
    T *end;
};

elements:指向陣列的第一個元素;first_free:指向最後一個實際元素之後的那個元素;end:指向陣列本身之後的那個元素。看下面這張圖可能更清楚一點。

template <class T> void VECTOR<T>::push_back(const T& t)
{
    if (first_free == end)                    //確認是否有可用空間
    {
        reallocate();                        //分配新空間並複製現存元素
    }

    alloc.construct(first_free, t);            //構造新元素
    ++first_free;
}

下面是reallocate()的簡單實現:

template <class T> void VECTOR<T>::reallocate()
{
    ptrdiff_t size = first_free - elements;
    ptrdiff_t newCapacity = 2 * max(size, 1);

    T *newElement = alloc.allocate(newCapacity);                //分配兩倍記憶體

    uninitialized_copy(elements, first_free, newElement);        //原記憶體元素拷貝到新記憶體

    for (T *p = first_free; p != elements; )                    //原記憶體元素逆序呼叫解構函式
    {
        alloc.destroy(--p);
    }

    if (elements)
    {
        alloc.deallocate(elements, end - elements);                //撤銷原記憶體空間
    }

    elements = newElement;                                        //調整新記憶體空間指標指向
    first_free = elements + size;
    end = elements + newCapacity;
}

說明:本例只做簡單說明。如果你對vector或STL實現感興趣,可以拜讀《STL原始碼分析》這本書,我也從這本書學到很多知識。

 

2、operator new函式和operator delete函式

當執行string *sp = new string("initialized");時發生三個步驟:

(1)呼叫名為operator new的標準庫函式,分配足夠大的原始的未型別化的記憶體,以儲存指定型別的一個物件。

(2)執行該型別的一個建構函式,用指定初始化式構造物件。

(3)返回指向新分配並構造的物件的指標。

當執行delete sp;時發生兩個步驟:

(1)對sp指向的物件執行適當的解構函式。

(2)呼叫名為operator delete的標準庫函式釋放該物件所用記憶體。

operator new和operator delete函式有兩個過載版本,每個版本支援相關的new操作:

void *operator new(size_t);

void *operator new[](size_t);

void *operator delete(size_t);

void *operator delete[](size_t);

說明:雖然operator new和operator delete的設計意圖是供new操作符使用,但它們也是標準庫中的函式,可使用它們獲得未構造的記憶體。舉例如下:

    T *newElement = alloc.allocate(newCapacity);                //分配兩倍記憶體
    T *newElement = static_cast<T*>(operator new[](sizeof(T) * newCapacity));

上面兩條語句是等價的,下面這兩條語句也是等價的。

    alloc.deallocate(elements, end - elements);                //撤銷原記憶體空間
    operator delete[](elements);

說明:allocator類分配型別化的記憶體,使用時不必計算以位元組為單位所需的記憶體,也避免對operator new的返回值進行強制型別轉換。比直接使用operator new,operator delete更為安全。

 

二、物件構造和撤銷

C++提供了不同方法在原始記憶體中構造和撤銷物件:

(1)allocator類的成員construct和destroy。

(2)定位new表示式。

(3)直接呼叫物件的解構函式撤銷物件。撤銷物件並不釋放物件所在的記憶體。

(4)演算法uninitialized_copy和uninitialized_fill構造物件。

 

下面主要介紹定位new表示式(其他情況我們都見過了)。

定位new表示式在已分配的原始記憶體中初始化一個物件,它不分配記憶體,接受指向已分配但未構造記憶體的指標,並在該記憶體中初始化一個物件。定位new表示式的形式是:

new (place_address) type

new (place_address) type(initializer-list)

其中place_address必須為指標,initializer-list提供了一個可能為空的初始化列表。舉例如下:

alloc.construct(first_free, t);
new (first_free) T(t);

string *sp = alloc.allocate(2);
new (sp) string(b, e);

注意:

(1)定位new表示式初始化一個物件時,可使用任何建構函式,並直接建立物件。allocator類的construct成員總是使用拷貝建構函式。

(2)對於值型別而言,直接構造物件與構造臨時物件並進行拷貝沒有什麼區別,效能差別基本沒什麼意義。但對某些類而言,使用拷貝建構函式是不可能的(拷貝建構函式可能是私有的等),或應該避免的。這種情況,或許你應該考慮定位new表示式。

相關文章