標籤:儲存 位元組 call free 其他 int 類類型 記憶體配置 選擇
new為特定類型分配記憶體,並在新分配的記憶體中構造該類型的一個對象。new 運算式自動運行合適的建構函式來初始化每個動態分配的類類型對象。某些情況下,需要將記憶體配置與物件建構分離開。
使用 new運算式的時候,分配記憶體,並在該記憶體中構造一個對象:使用 delete 運算式的時候,調用解構函式撤銷對象,並將對象所用記憶體返還給系統。接管記憶體配置時,必須處理這兩個任務。
C++ 提供下面兩種方法分配和釋放未構造的原始記憶體。allocator 類;以及標準庫中的 operator new 和 operator delete函數。
C++ 還提供不同的方法在原始記憶體中構造和撤銷對象:
allocator 類定義了名為 construct 和 destroy 的成員,construct 成員在未構造記憶體中初始化對象,destroy 成員在對象上運行適當的解構函式。
定位 new 運算式接受指向未構造記憶體的指標,並在該空間中初始化一個對象或一個數組。可以直接調用對象的解構函式來撤銷對象。運行解構函式並不釋放對象所在的記憶體。
演算法 uninitialized_fill 和 uninitialized_copy 像 fill 和 copy演算法一樣執行,除了它們是在目的記憶體構造對象,而不是給對象賦值。
現代 C++ 程式一般應該使用 allocator 類來分配記憶體,它更安全更靈活。但是,在構造對象的時候,用 new 運算式比allocator::construct 成員更靈活。
一: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 classtemplate <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) typenew (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 iteratorsnew (sp) string(b, e); // construct directly in placealloc.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* 形參,該指標可以是null 指標。如果提供了 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));}
C++記憶體配置