C++內存分配

new爲特定類型分配內存,並在新分配的內存中構造該類型的一個對象。new 表達式自動運行合適的構造函數來初始化每一個動態分配的類類型對象。某些狀況下,須要將內存分配與對象構造分離開。算法

使用 new表達式的時候,分配內存,並在該內存中構造一個對象:使用 delete 表達式的時候,調用析構函數撤銷對象,並將對象所用內存返還給系統。接管內存分配時,必須處理這兩個任務。數組

 

C++ 提供下面兩種方法分配和釋放未構造的原始內存。allocator 類;以及標準庫中的 operator new 和 operator delete函數。安全

 

C++ 還提供不一樣的方法在原始內存中構造和撤銷對象:數據結構

allocator 類定義了名爲 construct 和 destroy 的成員,construct 成員在未構造內存中初始化對象,destroy 成員在對象上運行適當的析構函數。函數

定位 new 表達式接受指向未構造內存的指針,並在該空間中初始化一個對象或一個數組。能夠直接調用對象的析構函數來撤銷對象。運行析構函數並不釋放對象所在的內存。優化

算法 uninitialized_fill 和 uninitialized_copy 像 fill 和 copy算法同樣執行,除了它們是在目的內存構造對象,而不是給對象賦值。ui

現代 C++ 程序通常應該使用 allocator 類來分配內存,它更安全更靈活。可是,在構造對象的時候,用 new 表達式比allocator::construct 成員更靈活。spa

 

一:allocator設計

allocator 類是一個模板,它提供類型化的內存分配以及對象構造與撤銷。指針

allocator<T> a

定義名爲 a 的 allocator 對象,能夠分配內存或構造 T 類型的對象

a.allocate(n)

分配原始的未構造內存以保存 T 類型的 n 個對象

a.deallocate(p, n)

釋放內存,在名爲 p 的 T* 指針中包含的地址處保存 T 類型的 n 個對象。運行調用 deallocate 以前在該內存中構造的任意對象的 destroy 是用戶的責任

a.construct(p, t)

在 T* 指針 p 所指內存中構造一個新元素。運行 T 類型的複製構造函數用 t 初始化該對象

a.destroy(p)

運行 T* 指針 p 所指對象的析構函數

uninitialized_copy(b, e, b2)

從迭代器 b 和 e 指出的輸入範圍將元素複製到從迭代器b2 開始的未構造的原始內存中。該函數在目的地構造元素,而不是給它們賦值。假定由 b2 指出的目的地足以保存輸入範圍中元素的副本

uninitialized_fill(b, e, t)

將由迭代器 b 和 e 指出的範圍中的對象初始化爲 t 的副本。假定該範圍是未構造的原始內存。使用複製構造函數構造對象

uninitialized_fill_n(b, e, t, n)

將由迭代器 b 和 e 指出的範圍中至多 n 個對象初始化爲 t 的副本。假定範圍至少爲 n 個元素大小。使用複製構造函數構造對象

allocator 類將內存分配和對象構造分開。當 allocator 對象分配內存的時候,它分配適當大小並排列成保存給定類型對象的空間。可是,它分配的內存是未構造的。

 

下面將模擬實現 vector 的一部分,定義名爲Vector的類來展現的allocator使用:

// pseudo-implementation of memory allocation strategy for a vector-like class
template <class T> class Vector {
public:
    Vector(): elements(0), first_free(0), end(0) { }
    void push_back(const T&);
    // ...
private:
    static std::allocator<T> alloc; // object to get raw memory
    void reallocate(); // get more space and copy existing elements
    T* elements; // pointer to first element in the array
    T* first_free; // pointer to first free element in the array
    T* end; // pointer to one past the end of the array
    // ...
};

 

每一個 Vector<T> 類型定義一個 allocator<T> 類型的 static 數據成員,以便在給定類型的 Vector 中分配和構造元素。每一個 Vector 對象在指定類型的內置數組中保存其元素,並維持該數組的下列三個指針:

• elements,指向數組的第一個元素。

• first_free,指向最後一個實際元素以後的那個元素。

• end,指向數組自己以後的那個元素。

下圖說明了這些指針的含義

 

 

push_back 使用這些指針將新元素加到 Vector 末尾:

template <class T>
void Vector<T>::push_back(const T& t)
{
    // are we out of space?
    if (first_free == end)
        reallocate(); // gets more space and copies existing elements to it
    alloc.construct(first_free, t);
    ++first_free;
}

 

首先肯定是否有可用空間,若是沒有,就調用 reallocate函數,分配新空間並複製現存元素,將指針重置爲指向新分配的空間。

一旦還有空間容納新元素,它就調用alloc.construct 函數使用類型 T 的複製構造函數將 t 值複製到由 first_free 指出的元素,而後,將 first_free 加 1 以指出又有一個元素在用。

 

下面是reallocate函數的實現:

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

    T* newelements = alloc.allocate(newcapacity);
    uninitialized_copy(elements, first_free, newelements);
    
    // destroy the old elements in reverse order
    for (T *p = first_free; p != elements; /* empty */ )
        alloc.destroy(--p);

    if (elements)
        alloc.deallocate(elements, end - elements);
    
    elements = newelements;
    first_free = elements + size;
    end = elements + newcapacity;
}

 

函數首先計算當前在用的元素數目,將該數目翻倍,並請求 allocator 對象來得到所需數量的空間。若是 Vector 爲空,就分配兩個元素。

uninitialized_copy 調用使用標準 copy 算法的特殊版本。這個版本但願目的地是原始的未構造內存,它在目的地複製構造每一個元素,而不是將輸入範圍的元素賦值給目的地,使用 T 的複製構造函數從輸入範圍將每一個元素複製到目的地。

for 循環對舊數組中每一個對象調用 allocator 的 destroy 成員,它按逆序撤銷元素,從數組中最後一個元素開始,以第一個元素結束。destroy 函數運行T 類型的析構函數來釋放舊元素所用的任何資源。

一旦複製和撤銷了元素,就釋放原來數組所用的空間。傳給deallocate 一個零指針是不合法的。

最後,重置指針以指向新分配並初始化的數組。將 first_free 和 end指針分別置爲指向最後構造的元素以後的單元以及所分配空間末尾的下一單元。

 

二:operator new 函數和 operator delete 函數

下面將介紹怎樣用更基本的標準庫機制實現相同的策略

首先,須要對 new 和 delete 表達式怎樣工做有更多的理解。當使用 new表達式: string * sp = new string("initialized") 的時候,實際上發生三個步驟。首先,該表達式調用名爲 operator new 的標準庫函數,分配足夠大的原始的未類型化的內存,以保存指定類型的一個對象;接下來,運行該類型的一個構造函數,用指定初始化式構造對象;最後,返回指向新分配並構造的對象的指針。

當使用 delete 表達式 delete sp 刪除動態分配對象的時候,發生兩個步驟。首先,對 sp 指向的對象運行適當的析構函數;而後,經過調用名爲 operator delete 的標準庫函數釋放該對象所用內存。

注意,new和delete表達式,與operator new和operator delete標準庫函數是兩回事。

 

1:operator new和operator delete標準庫函數

operator new 和 operator delete 函數有兩個重載版本,每一個版本支持相關的new 表達式和 delete 表達式:

void *operator new(size_t); // allocate an object

void *operator new[](size_t); // allocate an array

void *operator delete(void*); // free an object

void *operator delete[](void*); // free an array

 

雖然 operator new 和 operator delete 函數的設計意圖是供 new 和delete表達式使用,但它們一般是標準庫中的可用函數。可使用它們得到未構造內存,它們有點相似 allocate 類的 allocator 和 deallocate 成員。例如:

T* newelements = alloc.allocate(newcapacity);

 

這能夠從新編寫爲:

T* newelements = static_cast<T*> (operator new[](newcapacity * sizeof(T)));

 

 

相似地:

alloc.deallocate(elements, end - elements);

 

能夠從新編寫爲

operator delete[](elements);

 

 

這些函數的表現與 allocate 類的 allocator 和 deallocate 成員相似。可是,它們在一個重要方面有不一樣:它們在 void* 指針而不是類型化的指針上進行操做。allocate 成員分配類型化的內存,因此使用它的程序能夠沒必要以字節爲單位計算所需的內存量,也能夠避免對 operator new 的返回值進行強制類型轉換;相似地,deallocate 釋放特定類型的內存,也沒必要轉換爲 void*。

通常而言,使用 allocator 比直接使用operator new 和 operator delete 函數更爲類型安全。

 

2:定位new 表達式

標準庫函數 operator new 和 operator delete 是 allocator 的allocate 和 deallocate 成員的低級版本,它們都分配但不初始化內存。

allocator 的成員 construct 和 destroy 也有兩個低級選擇,相似於 construct 成員,有第三種 new 表達式,稱爲定位 new。定位 new表達式不分配內存,而是在已分配的原始內存中初始化一個對象。定位 new 表達式的形式是:

new (place_address) type
new (place_address) type (initializer-list)

 

其中 place_address 必須是一個指針,而 initializer-list 提供了(可能爲空的)初始化列表,以便在構造新分配的對象時使用。

 

可使用定位 new 表達式代替 Vector 實現中的 construct 調用。原來的代碼:

alloc.construct (first_free, t);

 

能夠用等價的定位 new 表達式代替

new (first_free) T(t);

 

定位 new 表達式比 allocator 類的 construct 成員更靈活。定位 new 表達式初始化一個對象的時候,它可使用任何構造函數,並直接創建對象。而construct 函數老是使用複製構造函數。

 

例如,能夠用下面兩種方式之一,從一對迭代器初始化一個已分配但未構造的 string 對象:

allocator<string> alloc;
string *sp = alloc.allocate(2); 
// two ways to construct a string from a pair of iterators
new (sp) string(b, e); // construct directly in place
alloc.construct(sp + 1, string(b, e)); // build and copy a temporary

 

定位 new 表達式使用了接受一對迭代器的 string 構造函數,在 sp 指向的空間直接構造 string 對象。當調用 construct 函數的時候,必須首先從迭代器構造一個 string 對象,以得到傳遞給 construct 的 string 對象,而後,該函數使用 string 的複製構造函數,將那個未命名的臨時 string 對象複製到sp 指向的對象中。

 

3:顯式析構函數的調用

可使用析構函數的顯式調用做爲調用 destroy 函數的低級選擇。在使用 allocator 對象的 Vector 版本中,經過調用 destroy 函數清除每一個元素:

for (T *p = first_free; p != elements; /* empty */ )
    alloc.destroy(--p);

 

對於使用定位 new 表達式構造對象的程序,顯式調用析構函數:

for (T *p = first_free; p != elements; /* empty */ )
    p->~T(); // call the destructor

 

顯式調用析構函數的效果是適當地清除對象自己。可是,並無釋放對象所佔的內存,若是須要,能夠重用該內存空間。調用 operator delete 函數不會運行析構函數,它只釋放指定的內存。

 

三:類特定的 new 和 delete

前幾節介紹了類怎樣可以接管本身的內部數據結構的內存管理,另外一種優化內存分配的方法涉及優化 new 表達式的行爲。

默認狀況下,new 表達式經過調用由標準庫定義的 operator new 版本分配內存。經過定義本身的名爲 operator new 和 operator delete 的成員,類能夠管理用於自身類型的內存。編譯器看到類類型的 new 或 delete 表達式的時候,它查看該類是否有operator new 或 operator delete 成員,若是類定義(或繼承)了本身的成員new 和 delete 函數,則使用那些函數爲對象分配和釋放內存;不然,調用這些函數的標準庫版本。

 

類成員 operator new 函數必須具備返回類型 void* 並接受 size_t 類型的形參。由 new 表達式用以字節計算的分配內存量初始化函數的 size_t 形參。

類成員 operator delete 函數必須具備返回類型 void。它能夠定義爲接受單個 void* 類型形參,也能夠定義爲接受兩個形參,即 void* 和 size_t 類型。由 delete 表達式用被 delete 的指針初始化 void* 形參,該指針能夠是空指針。若是提供了 size_t 形參,就由編譯器用第一個形參所指對象的字節大小自動初始化 size_t 形參。

除非類是某繼承層次的一部分,不然形參 size_t 不是必需的。當 delete指向繼承層次中類型的指針時,指針能夠指向基類對象,也能夠指向派生類對象。派生類對象的大小通常比基類對象大。若是基類有 virtual 析構函數,則傳給 operator delete 的大小將根據被刪除指針所指對象的動態類型而變化;若是基類沒有 virtual 析構函數,那麼,經過基類指針刪除指向派生類對象的指針的行爲,跟往常同樣是未定義的。

這些函數隱式地爲靜態函數,沒必要顯式地將它們聲明爲static,雖然這樣作是合法的。成員 new 和 delete 函數必須是靜態的,由於它們要麼在構造對象以前使用(operator new),要麼在撤銷對象以後使用(operator delete),所以,這些函數沒有成員數據可操縱。像任意其餘靜態成員函數同樣,new 和 delete 只能直接訪問所屬類的靜態成員。

 

也能夠定義成員 operator new[] 和 operator delete[] 來管理類類型的數組。若是這些 operator 函數存在,編譯器就使用它們代替全局版本。類成員 operator new[] 必須具備返回類型 void*,而且接受的第一個形參類型爲 size_t。表示存儲特定類型給定數目元素的數組的字節數值自動初始化操做符的 size_t 形參。

成員操做符 operator delete[] 必須具備返回類型 void,而且第一個形參爲 void* 類型。用表示數組存儲起始位置的值自動初始化操做符的 void* 形參。

類的操做符 delete[] 也能夠有兩個形參,第二個形參爲 size_t。若是提供了附加形參,由編譯器用數組所需存儲量的字節數自動初始化這個形參。

 

若是類定義了本身的成員 new 和 delete,類的用戶就能夠經過使用全局做用域肯定操做符,強制 new 或 delete 表達式使用全局的庫函數。若是用戶編寫

Type *p = ::new Type; // uses global operator new
::delete p; // uses global operator delete

 

那麼,即便類定義了本身的類特定的 operator new,也調用全局的 operator new;delete 相似。

若是用 new 表達式調用全局 operator new 函數分配內存,則delete 表達式也應該調用全局 operator delete 函數。

 

下面是一個定義了成員 new 和 delete的類的例子

class testalloc
{
public:
    testalloc(int a = 0): m_int(a),next(NULL){}
    void * operator new(size_t len);
    void operator delete(void *ptr);

private:
    int m_int;
    testalloc *next;

    static std::allocator<testalloc> alloc;
    static testalloc *freeptr;
    static int chunk;
    static void add_to_freelist(testalloc *ptr);
};

int testalloc::chunk = 100;
testalloc* testalloc::freeptr = NULL;
std::allocator<testalloc> testalloc::alloc;

void testalloc::add_to_freelist(testalloc *ptr)
{
    ptr->next = freeptr;
    freeptr = ptr;
}

void * testalloc::operator new(size_t len)
{
    testalloc *ptr;

    if (! freeptr)
    {
        std::cout << "alloc new memory" << std::endl;
        ptr = alloc.allocate(chunk);
        for (int i = 0; i < chunk; i++)
        {
            add_to_freelist(ptr+i);
        }
    }

    ptr = freeptr;
    freeptr = freeptr->next;
    return static_cast<void *>(ptr);
}

void testalloc::operator delete(void *ptr)
{
    if (ptr)
        add_to_freelist(static_cast<testalloc *>(ptr));
}
相關文章
相關標籤/搜索