new/delete 詳解
一、new/delete 簡介
new 和 delete 是 C++ 用於管理 堆記憶體 的兩個運算子,對應於 C 語言中的 malloc 和 free,但是 malloc 和 free 是函式,new 和 delete 是運算子。除此之外,new 在申請記憶體的同時,還會呼叫物件的建構函式,而 malloc 只會申請記憶體;同樣,delete 在釋放記憶體之前,會呼叫物件的解構函式,而 free 只會釋放記憶體。
new 運算子的內部實現分為兩步:
記憶體分配
呼叫相應的
operator new(size_t)
函式,動態分配記憶體。如果operator new(size_t)
new_handler()
函式用於處理new失敗問題。如果沒有設定new_handler()
函式或者new_handler()
未能分配足夠記憶體,則丟擲std::bad_alloc
異常。“new運算子”所呼叫的operator new(size_t)
函式,按照C++的名字查詢規則,首先做依賴於實參的名字查詢(即ADL規則),在要申請記憶體的資料型別T的 內部(成員函式)、資料型別T定義處的名稱空間查詢;如果沒有查詢到,則直接呼叫全域性的::operator new(size_t)
函式。建構函式
在分配到的動態記憶體塊上 初始化 相應型別的物件(建構函式)並返回其首地址。如果呼叫建構函式初始化物件時丟擲異常,則自動呼叫
operator delete(void*, void*)
delete 運算子的內部實現分為兩步:
解構函式
呼叫相應型別的解構函式,處理類內部可能涉及的資源釋放。
記憶體釋放
呼叫相應的
operator delete(void *)
函式。呼叫順序參考上述operator new(size_t)
函式(ADL規則)。
關於 new/delete 的內部實現,參考如下程式碼。
class T{
public:
T(){
cout << "建構函式。" << endl;
}
~T(){
cout << "解構函式。" << endl;
}
void * operator new(size_t sz){
T * t = (T*)malloc(sizeof(T));
cout << "記憶體分配。" << endl;
return t;
}
void operator delete(void *p){
free(p);
cout << "記憶體釋放。" << endl;
return;
}
};
int main()
{
T * t = new T(); // 先 記憶體分配 ,再 建構函式
delete t; // 先 解構函式, 再 記憶體釋放
return 0;
}
結果如下:
每個 new 獲取的物件,必須用 delete 析構並釋放記憶體,以免 記憶體洩漏。
舉例說明:
class Test{
public:
Test(){
str = new char[2];
}
~Test(){
delete [] str;
}
private:
char * str;
};
int main(){
// ①
Test * t = new Test;
free(t);
// ②
Test * t2 = (Test*)malloc(sizeof(Test));
delete t2;
return 0;
}
對於 ①,
new Test
的時候將會產生兩方面的記憶體:Test 物件本身的記憶體( Win32環境,4 Bytes 儲存 char * 指標);
str 所指向的 2 bytes 堆記憶體。
如果呼叫 free 釋放記憶體,那麼由於 free 並不會呼叫 Test 的解構函式,所以 free 只能釋放 Test 物件的記憶體(4 bytes),而 str 所指向的 2-bytes 堆記憶體並不能得到釋放,因此而造成 記憶體洩漏 。
對於 ②,malloc 並不會呼叫類的建構函式,所以只分配了 Test 物件的記憶體,str 並未初始化為指向一塊堆記憶體。所以當呼叫 delete 釋放記憶體的時候,將呼叫類的解構函式 (
delete [] str
),此時 delete 一塊沒有使用權的記憶體,程式崩潰 。
總之,編寫C++程式時,在進行動態記憶體分配的時候,最好使用 new 和 delete。並且記住,new 出來的物件用 delete “消滅”它。
二、new/delete 表示式語法
2.1 new 表示式語法
2.1.1 記憶體分配
1)普通的 new 運算子表示式
new 的基本語法 :
type * p_var = new type; // int * a = new int; // 分配記憶體,但未初始化,垃圾值
通過new初始化物件,使用下述語法:
type * p_var = new type(init); // int * a = new int(8); //分配記憶體時,將 *a 初始化為 8
其中 init 是傳遞給建構函式的實參表或初值。
2)動態生成物件陣列的 new 運算子表示式
new 也可建立一個物件陣列:
type p_var = new type [size]; // int * a = new int[3] ; // 分配了 3個 int 大小的連續記憶體塊, 但未初始化
C++98 標準規定,new 建立的物件陣列不能被顯式初始化, 陣列所有元素被預設初始化。如果陣列元素型別沒有預設初始化(預設建構函式),則編譯報錯。但 C++11 已經允許顯式初始化,例如:
int *p_int = new int[3] {1,2,3};
如此生成的物件陣列,在釋放時必須呼叫 delete []
表示式。
2.1.2 placement new 運算子表示式
placement new 運算子表示式 就是 在使用者指定的記憶體位置上構建新的物件 ,這個構建過程並不需要額外分配記憶體,只需要呼叫物件的建構函式即可。
placement new 的語法是:
new ( expression-list ) new-type-id ( optional-initializer-expression-list );
使用這種 placement new 運算子表示式,原因之一是 使用者的程式不能在一塊記憶體上自行呼叫其建構函式,必須由編譯系統生成的程式碼呼叫建構函式。原因之二是可能需要把物件放在特定硬體的記憶體地址上,或者放在多處理器核心的共享的記憶體地址上。(PS:建構函式沒辦法直接這麼呼叫 p->A(),而解構函式可以直接這麼呼叫 p->~A()。)
釋放這種 placement new 運算子物件時,不能呼叫 placement delete,應直接呼叫解構函式,如:pObj->~ClassType() ; 然後再自行釋放記憶體。
注意: C++ 中並沒用與 placement new 運算子 功能相對應 的 placement delete 運算子(沒有placement delete 運算子的概念,但是有 placement delete 函式)。^_^
解釋:
-
class Arena { public: void * allocate(size_t); void deallocate(void\*); .... }; void * operator new(size_t sz, Arena& a) { return a.allocate(sz); } Arena a1(some arguments); Arena a2(some arguments); X* p1 = new(a1) X; Y* p2 = new(a1) Y; Z* p3 = new(a2) Z;
對於上述程式碼,C++的型別機制並不能推斷 p1 指向的物件是否位於 a1 之上。那麼直接呼叫
delete(a1) p1;
就容易出錯。所以為了安全,C++不提供 placement delete 運算子。 placement new 運算子不另外分配記憶體,換句話說,不是new運算子。它完成的功能是在給定地址上呼叫建構函式。如果提供p->T(),那麼 placement new 運算子就不需要了。如果存在功能對應的 placement delete 運算子,那麼功能就應該是在給定地址上呼叫解構函式。但因為 C++已經提供了p->~T(),就沒必要有 placement delete 運算子。
如果存在對應的 placement delete 運算子,其實就是呼叫解構函式。而本身解構函式就可以自行主動呼叫,那麼自己呼叫就好了,但是物件本身所佔用這塊記憶體還可以繼續使用。如果想 placement delete 運算子像打洞一樣,連物件記憶體一起回收,那
operator new(size_t )
的大塊蜂窩煤記憶體如何 delete 。這不科學,既然整塊記憶體是operator new(size_t)
的,就應該由operator delete(void *)
回收,而不能用 placement delete 運算子部分回收。總之,沒有與 placement new 運算子功能相對應的 placement delete 運算子。而且需要注意的是,運算子和函式是兩個不同的概念,C++有 placement new 運算子和函式的概念,但是沒有 placement delete 運算子的概念,有 placement delete 函式的概念 。
所以,對於 placement new 運算子,我們需要主動呼叫物件的解構函式。如下示例:
#include <iostream>
using namespace std;
class Test{
public:
Test(){
cout << "Test 構造" << endl;
str = new char[2];
}
~Test(){
cout << "Test 析構" << endl;
delete [] str;
}
private:
char * str;
};
int main(int argc, char* argv[])
{
char buf[100]; // 棧變數
Test *p = new(buf) Test(); // Test()產生的臨時變數用於初始化 指定記憶體地址
p->~Test(); // 一定要主動呼叫解構函式,避免記憶體洩漏。 而且呼叫必須在 buf 生命週期內呼叫才有效。
// buf 指向的棧記憶體並不需要程式設計師主動釋放。
// 棧變數過了生命週期會自動釋放記憶體
// 其實棧記憶體的釋放也不叫記憶體釋放,只是棧頂指標移動,如果該塊棧記憶體沒有被其他程式重新整理,那麼該棧記憶體的值依然不變。
char * buf2 = new char[100];
Test * p2 = new(buf2) Test();
p2->~Test(); // 切記,主動呼叫解構函式
delete [] buf2; // 堆記憶體需要主動釋放
return 0;
}
如上程式碼,如果把 p->~Test();
註釋掉,上述程式碼的結果將為:
Test 構造
顯然是隻呼叫建構函式。所以對於placement new,我們需要主動呼叫物件的解構函式 pObj->~ClassType()
。
2.1.3 如何在棧上new?
我們知道,new 是用於管理堆記憶體,那又怎麼可能在棧上 new 出一個物件呢?
通過上面的討論,我們發現,new 除了能用於動態分配記憶體,還能夠使用 placement new 在特定記憶體位置進行初始化。所以,如何在棧上 new 呢?上述程式碼(2.1.2)就是一個很好的例子。
2.1.4 不丟擲異常的new運算子
在分配記憶體失敗時,new運算子的標準行為是丟擲std::bad_alloc
異常。也可以讓new運算子在分配記憶體失敗時不丟擲異常而是返回空指標。
new (nothrow) Type ( optional-initializer-expression-list );
或
new (nothrow) Type[size]; // new (std::nothrow_t) Type[size];
其中 nothrow 是 std::nothrow_t
的一個例項.
2.2 delete 表示式語法
2.2.1 記憶體釋放
1)普通的 delete 運算子
delete 的基本語法是:
delete val_ptr;
2)釋放物件陣列的 delete 運算子
delete [] val_ptr
2.2.2 沒有 placement delete 運算子表示式
通過上面的討論,我們可以知道 C++ 中並沒有提供與 placement new 運算子功能相對應 placement delete 運算子。但是仍然有placement delete函式的概念,功能在後面有介紹。
C++ 不能使用 placement delete 運算子表示式直接析構一個物件但不釋放其記憶體。因此,對於placement new表示式構建的物件,析構釋放時有兩種辦法:
是直接寫一個函式,完成析構物件、釋放記憶體的操作:
void destroy (T * p, A & arena) { // *p 是在 arena 之上構建的物件,即 T * a = new(&arena) T; p->~T() ; // 先析構 *p 物件 arena.deallocate(p) ; // 再釋放 arena 整個記憶體,而不是位於arena中的部分記憶體(*p) } A arena ; T * p = new (arena) T ; .... destroy(p, arena) ;
分兩步顯式 呼叫解構函式 與 帶位置的 operator delete 函式:
A arena ; T * p = new (arena) T ; /* ... */ p->~T() ; // 先析構 operator delete(p, arena) ; // 呼叫 placement delete 函式(非運算子) // Then call the deallocator function indirectly via operator delete(void *, A &) .
帶位置的 operator delete(void *,void *)
函式,可以被 placement new 運算子表示式自動呼叫。這是在物件的建構函式丟擲異常的時候,用來釋放掉 placement new 函式獲取的記憶體(類內部可能涉及的記憶體分配)。以避免記憶體洩露。
#include <cstdlib>
#include <iostream>
char buf[100];
struct A {} ;
struct E {} ;
class T {
public:
T()
{
std::cout << "T 建構函式。" << std::endl;
throw E(); //丟擲異常
}
void * operator new(std::size_t,const A &)
{
std::cout << "Placement new called for class T." << std::endl;
return buf;
}
void operator delete(void*, const A &)
{
std::cout << "Placement delete called for class T." << std::endl;
}
} ;
void * operator new ( std::size_t, const A & )
{
std::cout << "Placement new called." << std::endl;
return buf;
}
void operator delete ( void *, const A & )
{
std::cout << "Placement delete called." << std::endl;
}
int main ()
{
A a ;
try {
T * p = new (a) T ;
/* do something */
}
catch (E exp)
{
std::cout << "Exception caught." << std::endl;
}
return 0 ;
}
結果如下:
C++ 有 placement delete 函式,但是沒有 placement delete 運算子的概念。
2.2.3 delete 類物件時該注意的問題
問題 1
如下一段程式碼,是否是產生記憶體洩漏? 此題的討論詳見 csdn 論壇 。
class A
{
public:
A(){}
virtual void f(){}
private:
int m_a;
};
class B : public A
{
public:
virtual void f(){}
private:
int m_b;
};
int main()
{
A *pa = new B;
delete pa;
pa = NULL;
return 0;
}
答案:不會產生記憶體洩漏。
delete 釋放記憶體時,會呼叫類的解構函式。 但是需要明確的是 解構函式並不會釋放 物件本身 的記憶體 。
delete 運算子分為2個階段。 第一個階段是呼叫類的解構函式,第二階段才是釋放物件記憶體(但是這個工作不是解構函式在做)。
解構函式是free()之前的呼叫,而真正釋放記憶體的操作是
free(void *ptr)
,注意只有指標一個引數,沒有長度引數,這說明了什麼?說明了A *pa = new B;
時帶著長度sizeof(B)最終呼叫了malloc(sizeof(B))
;申請的記憶體及長度已經被記錄,當free(pa)是就會釋放掉自pa開始長度為sizeof(B)的記憶體。解構函式僅僅是應用邏輯層次的釋放資源,不是物理層次的釋放資源。(PS:關於new/delete運算子的具體實現後面還會涉及。)
問題 2
修改一下上面的題目,如下是否會造成記憶體洩漏呢?
class A
{
public:
A(){
m_a = new int(1);
}
~A(){ // 宣告為virtual, 防止記憶體洩漏
delete m_a;
}
private:
int * m_a;
};
class B : public A
{
public:
B() : A(){
m_b = new int(2);
}
~B(){
delete m_b;
}
private:
int * m_b;
};
int main()
{
A * pa = new B;
delete pa;
pa = NULL;
return 0;
}
答案:會造成記憶體洩漏。
delete pa 的時候,只會呼叫基類的解構函式。所以 m_b 指向的記憶體塊沒得到釋放。造成記憶體洩漏。
通過這個例子,應該深刻理解 解構函式的作用: 程式設計師處理類內部可能涉及的記憶體分配、資源釋放。而不是釋放類本身的記憶體。
三、operator new/delete() 的函式過載
平時使用 new 動態生成一個物件,實際上是呼叫了 new 運算子。
該運算子首先呼叫了operator new(std::size_t )
函式動態分配記憶體,然後呼叫型別的建構函式初始化這塊記憶體。 new / delete 運算子是不能被過載的,但是下述各種 operator new/delete()
函式既可以作為 1. 全域性函式過載,也可以作為 2. 類成員函式或 3. 作用域內的函式過載,即由程式設計者指定如何獲取記憶體。
3.1 普通的operator new/delete(size_t size)函式
new 運算子 首先呼叫 operator new(std::size_t )
函式動態分配記憶體。首先查詢 類內 是否有 operator new(std::size_t)
函式可供使用(即依賴於實參的名字查詢)。
operator new(size_t )
函式的引數是一個 size_t 型別,指明瞭需要分配記憶體的規模。operator new(size_t )
函式可以被每個 C++ 類作為成員函式過載。也可以作為全域性函式過載:
void * operator new (std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
記憶體需要回收的話,呼叫對應的operator delete(void *)
函式。
例如,在 new 運算子表示式的第二步,呼叫建構函式初始化記憶體時如果丟擲異常,異常處理機制在棧展開(stack unwinding)時,要回收在new運算子表示式的第一步已經動態分配到的記憶體,這時就會 自動呼叫 對應 operator delete(void*)
函式。(注意:此處呼叫的是非位置delete函式)
struct E{};
class T{
public:
T(){
cout << "建構函式。" << endl;
throw E();
}
~T(){
cout << "解構函式。" << endl;
}
void * operator new(size_t sz){
T * t = (T*)malloc(sizeof(T));
cout << "記憶體分配。" << endl;
return t;
}
void operator delete(void *p){
free(p);
cout << "記憶體釋放。" << endl;
return;
}
};
int main()
{
try {
T * p = new T;
/* do something */
}
catch (E exp){
std::cout << "Exception caught." << std::endl;
}
return 0;
}
結果:
3.2 陣列形式的operator new/delete[](size_t size)函式
new type[] 運算子,用來動態建立一個物件陣列。這需要呼叫陣列元素型別內部定義的void* operator new[](size_t)
函式來分配記憶體。如果陣列元素型別沒有定義該函式,則呼叫全域性的void* operator new[](size_t)
函式來分配記憶體。
在 #include <new>
中聲明瞭 void* operator new[](size_t)
全域性函式:
void * operator new [] (std::size_t) throw(std::bad_alloc);
void operator delete [](void*) throw();
3.3 placement new/delete 函式
void * operator new(size_t,void*)
函式用於帶位置的 new 運算子呼叫。C++標準庫已經提供了operator new(size_t,void*)
函式的實現,包含 <new>
標頭檔案即可。這個實現只是簡單的把引數的指定的地址返回,帶位置的new運算子就會在該地址上呼叫建構函式來初始化物件:
// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) throw() { return __p; }
inline void* operator new[](std::size_t, void* __p) throw() { return __p; }
// Default placement versions of operator delete.
inline void operator delete (void*, void*) throw() { }
inline void operator delete[](void*, void*) throw() { }
禁止重定義這4個函式。因為都已經作為 <new>
的內聯函數了。在使用時,實際上不需要#include <new>
雖然上面的4個 placement new/delete 函式不能過載,但是仍然可以寫一個自己的 placement new/delete 函式,例如 :
inline void* operator new(std::size_t, A * /* 或者 const A &*/);
inline void* operator new[](std::size_t, A * /* 或者 const A &*/);
inline void operator delete (void*, A* /* 或者 const A &*/);
inline void operator delete[](void*, A* /* 或者 const A &*/);
但是,基本沒有什麼意義 ^_^。
3.4 保證不丟擲異常的operator new/delete函式
C++標準庫的<new>
中還提供了一個nothrow的實現,使用者可寫自己的函式替代:
void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();
void operator delete(void*, const std::nothrow_t&) throw();
void operator delete[](void*, const std::nothrow_t&) throw();
3.5 Clang關於operator new/delete 的實現
以下這段程式碼是Clang編譯器關於operator new(std::size_t)
和 operator delete (void *)
的實現:
void * operator new(std::size_t size) throw(std::bad_alloc) {
if (size == 0)
size = 1;
void* p;
while ((p = ::malloc(size)) == 0) {
std::new_handler nh = std::get_new_handler();
if (nh)
nh();
else
throw std::bad_alloc();
}
return p;
}
void operator delete(void* ptr) {
if (ptr)
::free(ptr);
}
這段程式碼很簡單,神祕的 operator new/delete 在背後也不過是在偷偷地呼叫C函式庫的 malloc / free !當然,這跟具體實現有關,Clang libcxx 是這樣實現,不代表其它實現也是如此。
需要意識到的是, operator new 和 operator + () 一樣,只不過是普通的函式,是可以過載的,所謂的 placement new ,也是一個全域性 operator new 的過載版本,在Clang libcxx 中定義如下:
inline _LIBCPP_INLINE_VISIBILITY void* operator new (std::size_t, void* __p) _NOEXCEPT
{
return __p;
}
四、小結
new 和 delete 是 C++ 用於管理 堆記憶體 的兩個運算子。
new 運算子 進行動態記憶體申請的時候,包含 2 個階段:
記憶體申請 new。
根據 Clang 的實現,我們可以猜測 記憶體new 基本就是通過 malloc 進行動態記憶體申請,但是本步驟並不初始化記憶體。本步驟對應
operator new(size_t )
函式。建構函式。
delete 運算子 進行記憶體釋放的時候,也包含 2 個階段:
析構物件。
記憶體釋放 delete。
本步驟對應
operator delete(void*)
函式。
除了用於記憶體管理的 new/delete 運算子,還有帶位置的 placement new 運算子,但是沒有帶位置的 placement delete 運算子。
placement new 運算子
解決不能主動呼叫建構函式的“矛盾”。
對應的函式是
operator new(size_t , void *)
。
placement delete 運算子
沒有此類運算子。
但有帶位置的 placement delete 函式,如全域性的
operator delete(void *,void*)
。
五、擴充套件 : free/delete 怎麼知道有多少記憶體要釋放 ?
在使用c或者c++的時候我們經常用到malloc/free和new/delete,在使用malloc申請記憶體的時候我們給定了需要申請的記憶體大小,但是在free或者delete的時候並不需要提供這個大小,那麼程式是怎麼實現準確無誤的釋放記憶體的呢?
實際上,在申請記憶體的時候,申請到的地址會比你實際的地址大一點點,他包含了一個存有申請空間大小的結構體。
比如你申請了20byte的空間,實際上系統申請了48bytes的block
- 16-byte header containing size, special marker, checksum, pointers to next/previous block and so on.
- 32 bytes data area (your 20 bytes padded out to a multiple of 16))
這樣在 free的時候就不需要提供任何其他的資訊,可以正確的釋放記憶體
這裡有個在 stackoverflow.com 上的提問,可以參考