定製new和delete(Cpp Operators of new and delete)

Cpp Operators of new and delete

1. 動態內存分配與釋放(new and delete)

一般說來,一個對象的生命期是由它被創建時所處的區域決定的。例如,在一對{}類定義的一個對象,在離開這個由{}所界定的區域時,該對象就會被銷燬,在這個區域之外這個對象是不存在的,程序的其他部分不能再引用這個對象了。

 

如果希望在離開了創建這個對象時所處的區域後,還希望這個對象存在並能繼續引用它,就必須用new操作符在自由存儲空間來分配一個對象。這個過程也叫做動態內存分配,也叫堆對象。任何由new操作符分配的對象都應該用delete操作符手動地銷燬掉。C++標準並沒有定義任何形式的“垃圾收集”機制。delete操作符只能用於由new返回的指針,或者是零。當delete的參數是0時,不會產生任何效果,也就是說這個delete操作符對應的函數根本就不會被執行。

 

newdelete)既可以分配(釋放)單個的對象(當然也包括內建類型),也可以分配(釋放)對象數組。下面是其函數原型:

#include <new>

void* operator new(size_t); // 參數是單個對象的大小

void* operator new[](size_t); // 參數是對象數組的總的大小

void delete(void*);

void delete[](void*);

C++標準中並沒有要求new操作符對分配出來的空間進行初始化。下面是使用new分配一個字符數組的例子:

char* save_string(const char* p)

{

char* s = new char[strlen(p)+1];

// ...

return s;

}

 

char* p = save_string(argv[1]);

// ...

delete[] p;

 

class X { /* ... */ }

X* p = new[10] X;

X* p2 = new X[10];

vector<int>* pv = new vector<int>(5);

new分配出足夠的空間後,編譯器會緊接着調用X的缺省構造函數對空間進行初始化。注意,上述兩種形式都是可以的,無論X是內建類型還是自定義用戶類型。對於類來說,還可以使用類的構造函數形式,如上面創建vector類型對象的例子。此外,一個用非數組形式的new操作符創建的對象,不能用數組形式的delete操作符來銷燬。

 

 

 

 

 

2. 提供自己的內存管理:重載new/delete操作符

我們可以爲new/delete定義自己的內存管理方式,但是替換全局的new/delete操作符的實現是不夠好的,原因很明顯:有些人可能需要缺省的new/delete操作的一些方面,而另一些人則可能完全使用另外一種版本的實現。所以最好的辦法是爲某個特定的類提供它自己的內存管理方式。

 

一個類的operator new()operator delete()成員函數,隱式地成爲靜態成員函數。因此它們沒有this指針,也不能修改對象(很好理解,當調用new的時候對象還沒有真正創建呢,當然不能修改對象了!)。當然在重載定義的時候,原型還是要與前面提到的一致。看下面這個例子:

void* Employee::operator new(size_t s)

{

// 分配s字節的內存空間,並返回這個空間的地址

 

void Employee::operator delete(void* p, size_t s)

{

// 假定指針p是指向由Employee::operator new()分配的大小爲s字節的內存空間。

// 釋放這塊空間以供系統在以後重用。

}

任何一個operator new()的操作符定義,都以一個尺寸值作爲第一個參數,且待分配對象的大小隱式給定,其值就作爲new操作符函數的第一個參數值。

 

在這裏分配空間的具體實現可以是多種多樣的,可以直接使用malloc/free(缺省的全局new/delete大部分都是用的這種),可以在指定的內存塊中分配空間(下節將要詳述),也可能還有其他的更好的更適合你的應用的方式。

 

那麼如何重載數組形式的new[]/delete[]操作符呢?與普通形式一樣,只不過delete[]的參數形式稍有不同,如下所示:

class Employee {

public:

void* operator new[](size_t);

void operator delete[](void*); // 單參數形式,少了一個size_t參數

void operator delete[](void*,size_t); //兩個參數形式也是可以的,但無必要

// ...

};

 

在編譯器的內部實現中,傳入new/delete[]的尺寸值可能是數組的大小s加上一個delta。這個delta量是編譯器的內部實現所定義的某種額外開銷。爲什麼delete操作符不需要第二個尺寸參數呢?因爲這個數組的大小s以及delta量都由系統“記住”了。但是delete[]的兩個參數形式的原型也是可以聲明的,在調用的時候會把s*sizeof(SomeClass)+delta作爲第二個參數值傳入。delta量是與編譯器實現相關的,因此對於用戶程序員來說是不必要知道的。故而這裏只提供單參數版本就可以了。(這倒是提供了一種查看這個delta量的方法。根據實際測試,GCC 4.1採用了4個字節的delta量。)

 

到這裏應該注意到,當我們調用operator delete()的時候,只給出了指針,並沒有給出對象大小的參數。那麼編譯器是怎麼知道應該給operator delete()提供正確的尺寸值的呢?如果delete參數類型就是該對象的確切型別,那麼這是一個簡單的事情,但是事情並不是總是這樣。看下面的例子:

class Manager : public Employee {

int level;

// ...

};

void f()

{

Employee* p = new Manager; // 麻煩:確切型別丟失了!

delete p;

}
這個時候編譯器不能得到正確的對象的尺寸。這就需要用戶的幫助了:只需要將基類的析構函數聲明稱爲虛函數即可。

 

3. 在指定位置安放對象(Placement of Objects)

new操作符的缺省方式是在自由內存空間中創建對象。如果希望在指定的地方分配對象,就應該使用這裏介紹的方法。看下面的例子:

class X {

public:
X(int);
//...
};
當需要把對象放置到指定地方的時候,只需要爲分配函數提供一個額外的參數(既指定的某處內存的地址),然後在使用new的時候提供這樣的一個額外參數即可。看下面的例子:

void* operator new(size_t, void* p) { return p; } // 顯示安放操作符

void* buf = reinterpret_cast<void*>(0xF00F); // 某個重要的地址

X* p2 = new(buf) X; // buf地址處創建一個X對象,

// 實際調用函數operator new(sizeof(X),buf)

 

4. 內存分配失敗與new_handler

如果new操作符不能分配出內存,會發生什麼呢?默認情況下,這個分配器會拋出一個bad_alloc異常對象。看下面的例子:

void f()

{
try{

for(;;) new char [10000];

}

catch(bad_alloc) {

cerr << "Memory exhausted!/n";

}
}
[疑問:構造一個異常對象也需要內存空間,既然已經內存耗盡了,那這個內存又從哪裏來呢?]
可以自定義內存耗盡時的處理方法(new_handler)。當new操作失敗時,首先會調用一個由set_new_handler()指定的函數。我們可以自定義這個函數,然後用set_new_handler()來登記。最後當new操作失敗時可以調用適當的自定義處理過程。看下面的例子:

#include <new> // set_new_handler()原型在此頭文件中

void out_of_store()

{

cerr << "operator new failed: out of store/n";

throw bad_alloc();

}

 

set_new_handler(out_of_store);

for(;;) new char[10000];

cout << "done/n";

 

上述例子中控制流不會到達最後一句輸出,也就是說永遠不會輸出done。而是會輸出:

operator new failed: out of store

自定義的new_handler函數的原型如下:

typedef void (*new_handler)();

 

 

 

5. 標準頭文件<new>中的原型

下面是標準頭文件中的各種原型聲明:

class bad_alloc : public exception { /* ... */ }

 

struct nothrow_t { };

extern struct nothrow_t nothrow; // 內存分配器將不會拋出異常

 

typedef void (*new_handler)();

new_handler set_new_handler(new_handler new_p) throw();

 

1)普通的內存分配,失敗時拋出bad_alloc異常

// 單個對象的分配與釋放

void* operator new(size_t) throw(bad_alloc);

void operator delete(void*) throw();

// 對象數組分配與釋放

void* operator new[](size_t) throw(bad_alloc);

void operator delete[](void*) throw();

 

2)與C方式兼容的內存分配,失敗時返回0,不拋出異常

// 單個對象分配與釋放

void* operator new(size_t, const nothrow_t&) throw();

void operator delete(void*, const nothrow_t&) throw();

// 對象數組分配與釋放

void* operator new[](size_t, const nothrow_t&) throw();

void operator delete[](void*, const nothrow_t&) throw();

 

3)從指定空間中分配內存

// 分配已有空間給單個對象使用

void* operator new(size_t, void* p) throw() { return p; }

void operator delete(void* p, void*) throw() { } //什麼都不做!

// 分配已有空間給對象數組使用

void* operator new[](size_t, void* p) throw() {return p;}

void operator delete[](void* p, void*) throw() { } //什麼也不做!

 

在上述原型中,拋出空異常的函數都沒有辦法通過拋出std::bad_alloc發出內存耗盡的信號;它們在內存分配失敗時返回0

 

上述原型的使用方法:

class X {

public:

X(){};

X(int n){};

// ...

};
1)可以拋出異常的new/delete操作符。
原型的第一個參數,即對象(或對象數組)的大小,因此在使用時如下所示:

X* p = new X;

X* p1 = new X(5);

X* pa = new X[10];

分配對象數組時要注意:只能用這種形式,不能用帶參數的形式,例如下面的方式是錯誤的:

X* pa2 = new[20] X(5);

你想分配一個X數組,每個數組元素都用5進行初始化,這是不能做到的。

 

2)不拋出異常而返回0new/delete操作符

原型的第二個參數要求一個nothrow_t的引用,因此必須以<new>中定義的nothrow全局對象作爲new/delete的參數,如下所示:

void f()

{

int* p = new int[10000]; // 可能會拋出bad_alloc異常

 

if(int* q = new(nothrow) int[100000]; {

// 內存分配成功

delete(nothrow)[]q;
}

else {

// 內存分配失敗

}
}

 

6. new與異常

如果在使用new構造對象時,構造函數拋出了異常,結果會怎樣?由new分配的內存釋放了嗎?在通常情況下答案是肯定的;但如果是在指定位置上分配對象空間,那麼答案就不是這麼簡單了。如果這個內存塊是由某個類的new函數分配的,那麼就會調用其相應的delete函數(如果有的話),否則不會有釋放內存的動作發生。這種策略很好地處理了標準庫中的使用指定內存的new操作符,以及提供了成對的分配與釋放函數的任何情形。
看下面這個例子:

void f(Arena& a, X* buffer)

{

X* p1 = new X;

X* p2 = new X[10];

X* p3 = new(buffer[10]) X;

X* p4 = new(buffer[11]) X[10];

X* p5 = new(a) X;

X* p6 = new(a) X[10];

}
分析:p1p2將能正確釋放其分配的內存,不會造成內存泄漏,這屬於一種正常情況。後面的四種情況則比較複雜。對象a如果是採用普通方式分配的內存,那麼將能夠正確釋放其擁有的內存。

 

7. malloc/free沒用了嗎?

new能夠完全替代malloc嗎?絕大部分情況下,答案都是肯定的。但是有一種情況則非用malloc不可了。根據new的定義,其第一個參數是待分配對象的大小,但在使用時不需要明確地給出這個值。這個值是由編譯器暗中替你完成的。倘若在某種情況下,需要在分配一個對象的同時還要分配出一些額外的空間用來管理某些相關的信息。這個額外空間與對象的空間要求連續。這個時候new就幫不上了。必須用malloc把對象和額外空間的總大小作爲malloc的參數。在分配出來了後,可能需要調用new的放置形式的調用在該塊內存上構造對象。

8. 垃圾收集

當我們爲自己的類提供了自己的內存管理方法時,有可能會出現內存分配失敗的情況。因此我們可能會通過set_new_handler()提供一個更靈巧的內存釋放與重用機制。這就爲實現垃圾收集提供了一個實現思路。垃圾收集機制的基本思想是,當一個對象不再被引用時,它的內存就可以安全地被新的對象所使用。
當比較垃圾收集機制與手工管理方式的代價時,從一下幾個方面進行比較:
運行時間,內存的使用,可靠性,移植性,編程的費用,垃圾收集器的費用,性能的預期。

 

垃圾收集器必須要處理幾個重要的問題:
1)指針的僞裝
通常若以非指針的形式來存儲一個指針,則把這個指針叫做“僞裝的指針”。看下面這個例子:

void f()

{

int* p = new int;

long i1 = reinterpret_cast<long>(p) & 0xFFFF0000;

long i2 = reinterpret_cast<long>(p) & 0x0000FFFF;

p = 0;

// 這裏就不存在指向那個整型數的指針了!

p = reinterpret_cast<int*>(i1|i2);

// 現在這個整型數又被引用了!

}
上例中原本由p持有的指針被僞裝成兩個整型數i1i2。垃圾收集器必須關注這種僞裝的指針。
指針僞裝還有另外一種形式,即同時有指針和非指針成員的union結構,也會給垃圾收集器帶來特殊的問題。看下面的例子:

union U {

int* p;

int i;

};

 

void f(U u, U u2, U u3)

{

u.p = new int;

u2.i = 99999;

u.i = 8;

// ...

}
通常這是不可能知道union中包含的是指針還是整數。

 

2delete函數
通常若使用了自動垃圾收集,那麼deletedelete[]函數是不再需要了的。但是deletedelete[]函數除了釋放內存的功能外,還會調用析構函數。因此在這種情況下,下列調用

delete p;

就只調用析構函數,而內存的複用則向後推遲,直到內存塊被收集。一次回收多個對象,有助於減少碎片。

 

3)析構函數
當垃圾收集器準備回收對象時,有兩種辦法可選:

[1] 爲這個對象調用析構函數(如果有的話);

[2] 將這個對象當作原始內存(即不調用析構函數)。

一般垃圾收集器會選擇第二中方法。這種方法gc就成爲模擬一種無限內存的機制。
也有可能設計一種gc,它能調用向它註冊了的對象的析構函數。這種設計的一個重要方面是防止析構函數重複刪除一個之前已經刪除的對象。

 

4)內存的碎片
處理內存碎片的問題上,有兩種主要的GC類型:拷貝型和保守型。拷貝型GC通過移動對象使得碎片空間緊湊;而保守型則通過分配方式的改善來減少碎片。C++的觀點看來,更傾向於保守型的。因爲移動對象將導致大量的指針和引用等失效,所以拷貝型GCC++中幾乎是不可能實現的。此外保守型GC也可以讓C++的代碼段與C代碼段共存。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章