1. 程式人生 > >C++ Memory System Part1: new和delete

C++ Memory System Part1: new和delete

part 也有 其中 oid 事情 oca ddr temp 高級工程師

在深入探索自定義內存系統之前,我們需要了解一些基礎的背景知識,這些知識點是我們接下裏自定義內存系統的基礎。所以第一部分,讓我們來一起深入了解一下C++的newdelete家族,這其中有很多令人吃驚的巧妙設計,甚至有很多高級工程師都其細節搞不清楚。

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

調用的是另一個重載的operator new函數:

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

line)

{

// 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 newplacement 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[]

到目前為止,我們只講解了newdelete的非數組版本,它們還有一對為數組分配內存的版本:

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[],deletedelete[]來實現自定義的內存管理。更多的內容可以看內存系統的第二部分。

C++ Memory System Part1: new和delete