C++內存分配與對象構造的分離
在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表達式。