C++ Memory System Part1: new和delete
在深入探索自定義內存系統之前,我們需要了解一些基礎的背景知識,這些知識點是我們接下裏自定義內存系統的基礎。所以第一部分,讓我們來一起深入了解一下C++的new和delete家族,這其中有很多令人吃驚的巧妙設計,甚至有很多高級工程師都其細節搞不清楚。
new operator and operator new
首先我們來看一個使用new的簡單語句:
T* i = new T;
這是一個new operator最簡單的用法,那麽該操作符到底做了些什麽呢?
- 首先,調用operator new為單個T分配內存
- 其次,在operator new返回的地址上調用T的構造函數,創建對象
如果T是C++的基礎類型,或者POD,或者沒有構造函數的類型,則不會調用構造函數,上面的語句就只是調用最簡單的operator new
void* operator new(size_t bytes);
編譯器會使用正確的字節大小來調用operator new,即sizeof(T).
到現在為止都還比較好理解,但是關於new operator的介紹還沒有結束,還有一個版本的new operator稱為placement new:
void* memoryAddress = (void*)0x100;
T* i = new (memoryAddress) T; // placement new
這是專門用來在特定的內存地址上構造對象的方法,也是唯一一個直接調用構造函數,而無需任何內存分配操作的方法。上面代碼的new operator
void* operator new(size_t bytes, void* ptr);
該形式的operator new並沒有分配任何內存,而是直接返回該指針。
placement new是一個非常強大的工具,因為利用它,我們可以重載我們自己的operator new,重載的唯一規則是operator new的第一個參數必須是size_t類型,編譯器會自動傳遞該參數,並根據參數選擇正確的operator new。
看下面這個例子:
void* operator new(size_t bytes, const char* file, int
{
// allocate bytes
}
// calls operator new(sizeof(T), __FILE__, __LINE__) to allocate memory
T* i = new (__FILE__, __LINE__) T;
拋開全局operator new和類operator new的區別不談,所有placement形式的new operator都可以歸結為以下形式:
// calls operator new(sizeof(T), a, b, c, d) to allocate memory
T* i = new (a, b, c, d) T;
等價於:
T* i = new (operator new(sizeof(T), a, b, c, d)) T;
調用operator new的魔法是由編譯器做了。此外,每一個重載的operator new都可以被直接調用。
我們也可以實現任意形式的重載,如果我們樂意,甚至可以使用模板:
template<class ALLOCATOR>
void* operator new(size_t bytes, ALLOCATOR& allocator, const char* file, int line)
{
returnallocator.Allocate(bytes);
}
這種形式的重載我們在後面的自定義allocator時會遇到,使用該形式的placement new,內存分配就可以使用不同的allocator,例如:
T* i = new (allocator, __FILE__, __LINE__) T;
delete operator / operator delete
對前面new出來的實例調用delete operator時,將會首先調用對象的析構函數,然後調用operator delete刪除內存。這點跟new的順序剛好是反的。這裏需要註意的一點是,無論我們使用的是那種形式的new來創建實例,都要使用一個對應版本的operator delete,看下面這個例子:
// calls operator new(sizeof(T), a, b, c, d)
// calls T::T()
T* i = new (a, b, c, d) T;
// calls T::~T()
// calls operator delete(void*)
delete i;
這裏會有一點繞,如果在程序正常運行時(即沒有發生異常時),你調用placement new 分配的內存是通過operator delete刪除的,如上所示,它不會調用placement delete。只有在placement new發生異常時(即分配了內存,但是還沒有來得及調用構造函數時發生異常),運行時系統才會去尋找匹配placement new的placement delete,如果這時你定義了匹配的placement delete則會正常的調用。如果你並沒有定義匹配的placement delete則系統什麽都不做,這就會導致內存泄漏。這部分知識在Effective C++第52條款中有詳細的論述。
跟operator new一樣,operator delete可以被直接調用,實例代碼:
template<class ALLOCATOR>
voidoperator delete(void* ptr, ALLOCATOR& allocator, const char* file, int line)
{
allocator.Free(ptr);
}
// call operator delete directly
operator delete(i, allocator, __FILE__, __LINE__);
這裏要註意,如果你是直接調了operator delete,那麽一定要記得在此之前手動調用對象的析構函數:
// call the destructor
i->~T();
// call operator delete directly
operator delete(i, allocator, __FILE__, __LINE__);
new[] / delete[]
到目前為止,我們只講解了new和delete的非數組版本,它們還有一對為數組分配內存的版本:
new[] / delete[]
從這裏開始,才是new和delete系列最有趣的地方,也是最容易被人忽略的地方,因為在這裏包含了編譯器的黑魔法。C++標準只是規定了new[]和delete[]應該做什麽,但是沒有說如何做,這如何實現就是編譯器自己的事情了。
先來看一個簡單的語句:
int* i = new int [3];
上面的代碼通過調用operator new[]為3個int分配了內存空間,因為int是一個內置類型,所以沒有構造函數可以調用。像operator new一樣,我們也可以重載operator new[],實現一個placement語法的版本:
// our own version of operator new[]
void* operator new[](size_t bytes, const char* file, int line);
// calls the above operator new[]
int* i = new (__FILE__, __LINE__) int [3];
delete[]和operator delete[]的行為跟delete和operator delete是一樣的,我們也可以直接調用operator delete[],但是必須記得手動調用析構函數。
但是,如果是非POD類型呢?
來看一個例子:
structTest
{
Test(void)
{
// do something
}
~Test(void)
{
// do something
}
inta;
};
Test* i = new (__FILE__, __LINE__) Test [3];
在上面的情況下,盡管sizeof(Test) == 4,我們分配了3個實例,但是operator new[]還是會使用一個16字節的參數來調用,為什麽呢?多出的4個字節從哪裏來的呢?
要想知道這是為什麽,我們要先想想數組應該如何被刪除:
delete[] i;
刪除數組,編譯器需要知道到底要刪除多少個Test實例,否則的話它沒辦法挨個調用這些實例的析構函數,所以,為了得到這個數據,大部分的編譯器是這麽實現new[]的:
- 對N個類型為T的實例,operator new[]需要為數組分配sizeof(T)*N + 4 bytes的內存
- 將N存儲在前4個字節
- 使用placement new從ptr + 4的位置開始,構造N個實例
- 返回ptr + 4處的地址給用戶
最後一點非常重要:如果你重載了operator new[],返回的內存地址為0x100,那麽實例Test* i這個指針指向的位置則是0x104!!!這16個字節的內存布局如下:
0x100: 03 00 00 00 -> number of instances stored by the compiler-generated code
0x104: ?? ?? ?? ?? -> i[0], Test* i
0x108: ?? ?? ?? ?? -> i[1]
0x10c: ?? ?? ?? ?? -> i[2]
當調用delete[]時,編譯器會插入代碼,從給定指針處減4個字節的位置讀取實例的數量N,然後再反序調用析構函數。如果是內置類型或者POD,則沒有這4個字節的內存,因為不需要調用析構函數。
不幸的是,這個編譯器定義的行為給我們自己重載使用operator new,operator new[].operator delete,operator delete[]帶來了問題,即使我們可以直接調用operator delete[],也需要通過某種方法獲取有多少個析構函數需要調用。
但是我們做不到!我們不能通過自己插入額外的字節來解決,因為我們不知道編譯器是否又插入了額外的四個字節,這完全是根據各個編譯器自己實現決定的,也許這樣做可以,但也有可能會導致程序崩潰。所以在實際的使用中,最好不要直接去調用operator delete[]。
通過以上的知識,我們了解到,我們可以在自定義的內存系統中,定義自己的allocator函數,然後在使用new, new[],delete和delete[]來實現自定義的內存管理。更多的內容可以看內存系統的第二部分。
C++ Memory System Part1: new和delete