[TC++PL] new/delete 操作符
Cpp Operators of new and delete
1. 動(dòng)態(tài)內(nèi)存分配與釋放(new and delete)
一般說來,一個(gè)對象的生命期是由它被創(chuàng)建時(shí)所處的區(qū)域決定的。例如,在一對{}類定義的一個(gè)對象,在離開這個(gè)由{}所界定的區(qū)域時(shí),該對象就會(huì)被銷毀,在這個(gè)區(qū)域之外這個(gè)對象是不存在的,程序的其他部分不能再引用這個(gè)對象了。
如果希望在離開了創(chuàng)建這個(gè)對象時(shí)所處的區(qū)域后,還希望這個(gè)對象存在并能繼續(xù)引用它,就必須用new操作符在自由存儲(chǔ)空間來分配一個(gè)對象。這個(gè)過程也叫做動(dòng)態(tài)內(nèi)存分配,也叫堆對象。任何由new操作符分配的對象都應(yīng)該用delete操作符手動(dòng)地銷毀掉。C++標(biāo)準(zhǔn)并沒有定義任何形式的“垃圾收集”機(jī)制。delete操作符只能用于由new返回的指針,或者是零。當(dāng)delete的參數(shù)是0時(shí),不會(huì)產(chǎn)生任何效果,也就是說這個(gè)delete操作符對應(yīng)的函數(shù)根本就不會(huì)被執(zhí)行。
new(delete)既可以分配(釋放)單個(gè)的對象(當(dāng)然也包括內(nèi)建類型),也可以分配(釋放)對象數(shù)組。下面是其函數(shù)原型:
#include <new>
void* operator new(size_t); // 參數(shù)是單個(gè)對象的大小
void* operator new[](size_t); // 參數(shù)是對象數(shù)組的總的大小
void delete(void*);
void delete[](void*);
C++標(biāo)準(zhǔn)中并沒有要求new操作符對分配出來的空間進(jìn)行初始化。下面是使用new分配一個(gè)字符數(shù)組的例子:
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分配出足夠的空間后,編譯器會(huì)緊接著調(diào)用X的缺省構(gòu)造函數(shù)對空間進(jìn)行初始化。注意,上述兩種形式都是可以的,無論X是內(nèi)建類型還是自定義用戶類型。對于類來說,還可以使用類的構(gòu)造函數(shù)形式,如上面創(chuàng)建vector類型對象的例子。此外,一個(gè)用非數(shù)組形式的new操作符創(chuàng)建的對象,不能用數(shù)組形式的delete操作符來銷毀。
2. 提供自己的內(nèi)存管理:重載new/delete操作符
我們可以為new/delete定義自己的內(nèi)存管理方式,但是替換全局的new/delete操作符的實(shí)現(xiàn)是不夠好的,原因很明顯:有些人可能需要缺省的new/delete操作的一些方面,而另一些人則可能完全使用另外一種版本的實(shí)現(xiàn)。所以最好的辦法是為某個(gè)特定的類提供它自己的內(nèi)存管理方式。
一個(gè)類的operator new()和operator delete()成員函數(shù),隱式地成為靜態(tài)成員函數(shù)。因此它們沒有this指針,也不能修改對象(很好理解,當(dāng)調(diào)用new的時(shí)候?qū)ο筮€沒有真正創(chuàng)建呢,當(dāng)然不能修改對象了!)。當(dāng)然在重載定義的時(shí)候,原型還是要與前面提到的一致。看下面這個(gè)例子:
void* Employee::operator new(size_t s)
{
// 分配s字節(jié)的內(nèi)存空間,并返回這個(gè)空間的地址
}
void Employee::operator delete(void* p, size_t s)
{
// 假定指針p是指向由Employee::operator new()分配的大小為s字節(jié)的內(nèi)存空間。
// 釋放這塊空間以供系統(tǒng)在以后重用。
}
任何一個(gè)operator new()的操作符定義,都以一個(gè)尺寸值作為第一個(gè)參數(shù),且待分配對象的大小隱式給定,其值就作為new操作符函數(shù)的第一個(gè)參數(shù)值。
在這里分配空間的具體實(shí)現(xiàn)可以是多種多樣的,可以直接使用malloc/free(缺省的全局new/delete大部分都是用的這種),可以在指定的內(nèi)存塊中分配空間(下節(jié)將要詳述),也可能還有其他的更好的更適合你的應(yīng)用的方式。
那么如何重載數(shù)組形式的new[]/delete[]操作符呢?與普通形式一樣,只不過delete[]的參數(shù)形式稍有不同,如下所示:
class Employee {
public:
void* operator new[](size_t);
void operator delete[](void*); // 單參數(shù)形式,少了一個(gè)size_t參數(shù)
void operator delete[](void*,size_t); //兩個(gè)參數(shù)形式也是可以的,但無必要
// ...
};
在編譯器的內(nèi)部實(shí)現(xiàn)中,傳入new/delete[]的尺寸值可能是數(shù)組的大小s加上一個(gè)delta。這個(gè)delta量是編譯器的內(nèi)部實(shí)現(xiàn)所定義的某種額外開銷。為什么delete操作符不需要第二個(gè)尺寸參數(shù)呢?因?yàn)檫@個(gè)數(shù)組的大小s以及delta量都由系統(tǒng)“記住”了。但是delete[]的兩個(gè)參數(shù)形式的原型也是可以聲明的,在調(diào)用的時(shí)候會(huì)把s*sizeof(SomeClass)+delta作為第二個(gè)參數(shù)值傳入。delta量是與編譯器實(shí)現(xiàn)相關(guān)的,因此對于用戶程序員來說是不必要知道的。故而這里只提供單參數(shù)版本就可以了。(這倒是提供了一種查看這個(gè)delta量的方法。根據(jù)實(shí)際測試,GCC 4.1采用了4個(gè)字節(jié)的delta量。)
到這里應(yīng)該注意到,當(dāng)我們調(diào)用operator delete()的時(shí)候,只給出了指針,并沒有給出對象大小的參數(shù)。那么編譯器是怎么知道應(yīng)該給operator delete()提供正確的尺寸值的呢?如果delete參數(shù)類型就是該對象的確切型別,那么這是一個(gè)簡單的事情,但是事情并不是總是這樣。看下面的例子:
class Manager : public Employee {
int level;
// ...
};
void f()
{
Employee* p = new Manager; // 麻煩:確切型別丟失了!
delete p;
}
這個(gè)時(shí)候編譯器不能得到正確的對象的尺寸。這就需要用戶的幫助了:只需要將基類的析構(gòu)函數(shù)聲明稱為虛函數(shù)即可。
3. 在指定位置安放對象(Placement of Objects)
new操作符的缺省方式是在自由內(nèi)存空間中創(chuàng)建對象。如果希望在指定的地方分配對象,就應(yīng)該使用這里介紹的方法。看下面的例子:
class X {
public:
X(int);
//...
};
當(dāng)需要把對象放置到指定地方的時(shí)候,只需要為分配函數(shù)提供一個(gè)額外的參數(shù)(既指定的某處內(nèi)存的地址),然后在使用new的時(shí)候提供這樣的一個(gè)額外參數(shù)即可。看下面的例子:
void* operator new(size_t, void* p) { return p; } // 顯示安放操作符
void* buf = reinterpret_cast<void*>(0xF00F); // 某個(gè)重要的地址
X* p2 = new(buf) X; // 在buf地址處創(chuàng)建一個(gè)X對象,
// 實(shí)際調(diào)用函數(shù)operator new(sizeof(X),buf)
4. 內(nèi)存分配失敗與new_handler
如果new操作符不能分配出內(nèi)存,會(huì)發(fā)生什么呢?默認(rèn)情況下,這個(gè)分配器會(huì)拋出一個(gè)bad_alloc異常對象。看下面的例子:
void f()
{
try{
for(;;) new char [10000];
}
catch(bad_alloc) {
cerr << "Memory exhausted!\n";
}
}
[疑問:構(gòu)造一個(gè)異常對象也需要內(nèi)存空間,既然已經(jīng)內(nèi)存耗盡了,那這個(gè)內(nèi)存又從哪里來呢?]
可以自定義內(nèi)存耗盡時(shí)的處理方法(new_handler)。當(dāng)new操作失敗時(shí),首先會(huì)調(diào)用一個(gè)由set_new_handler()指定的函數(shù)。我們可以自定義這個(gè)函數(shù),然后用set_new_handler()來登記。最后當(dāng)new操作失敗時(shí)可以調(diào)用適當(dāng)?shù)淖远x處理過程。看下面的例子:
#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";
上述例子中控制流不會(huì)到達(dá)最后一句輸出,也就是說永遠(yuǎn)不會(huì)輸出done。而是會(huì)輸出:
operator new failed: out of store
自定義的new_handler函數(shù)的原型如下:
typedef void (*new_handler)();
5. 標(biāo)準(zhǔn)頭文件<new>中的原型
下面是標(biāo)準(zhǔn)頭文件中的各種原型聲明:
class bad_alloc : public exception { /* ... */ }
struct nothrow_t { };
extern struct nothrow_t nothrow; // 內(nèi)存分配器將不會(huì)拋出異常
typedef void (*new_handler)();
new_handler set_new_handler(new_handler new_p) throw();
(1)普通的內(nèi)存分配,失敗時(shí)拋出bad_alloc異常
// 單個(gè)對象的分配與釋放
void* operator new(size_t) throw(bad_alloc);
void operator delete(void*) throw();
// 對象數(shù)組分配與釋放
void* operator new[](size_t) throw(bad_alloc);
void operator delete[](void*) throw();
(2)與C方式兼容的內(nèi)存分配,失敗時(shí)返回0,不拋出異常
// 單個(gè)對象分配與釋放
void* operator new(size_t, const nothrow_t&) throw();
void operator delete(void*, const nothrow_t&) throw();
// 對象數(shù)組分配與釋放
void* operator new[](size_t, const nothrow_t&) throw();
void operator delete[](void*, const nothrow_t&) throw();
(3)從指定空間中分配內(nèi)存
// 分配已有空間給單個(gè)對象使用
void* operator new(size_t, void* p) throw() { return p; }
void operator delete(void* p, void*) throw() { } //什么都不做!
// 分配已有空間給對象數(shù)組使用
void* operator new[](size_t, void* p) throw() {return p;}
void operator delete[](void* p, void*) throw() { } //什么也不做!
在上述原型中,拋出空異常的函數(shù)都沒有辦法通過拋出std::bad_alloc發(fā)出內(nèi)存耗盡的信號(hào);它們在內(nèi)存分配失敗時(shí)返回0。
上述原型的使用方法:
class X {
public:
X(){};
X(int n){};
// ...
};
(1)可以拋出異常的new/delete操作符。
原型的第一個(gè)參數(shù),即對象(或?qū)ο髷?shù)組)的大小,因此在使用時(shí)如下所示:
X* p = new X;
X* p1 = new X(5);
X* pa = new X[10];
分配對象數(shù)組時(shí)要注意:只能用這種形式,不能用帶參數(shù)的形式,例如下面的方式是錯(cuò)誤的:
X* pa2 = new[20] X(5);
你想分配一個(gè)X數(shù)組,每個(gè)數(shù)組元素都用5進(jìn)行初始化,這是不能做到的。
(2)不拋出異常而返回0的new/delete操作符
原型的第二個(gè)參數(shù)要求一個(gè)nothrow_t的引用,因此必須以<new>中定義的nothrow全局對象作為new/delete的參數(shù),如下所示:
void f()
{
int* p = new int[10000]; // 可能會(huì)拋出bad_alloc異常
if(int* q = new(nothrow) int[100000]; {
// 內(nèi)存分配成功
delete(nothrow)[]q;
}
else {
// 內(nèi)存分配失敗
}
}
6. new與異常
如果在使用new構(gòu)造對象時(shí),構(gòu)造函數(shù)拋出了異常,結(jié)果會(huì)怎樣?由new分配的內(nèi)存釋放了嗎?在通常情況下答案是肯定的;但如果是在指定位置上分配對象空間,那么答案就不是這么簡單了。如果這個(gè)內(nèi)存塊是由某個(gè)類的new函數(shù)分配的,那么就會(huì)調(diào)用其相應(yīng)的delete函數(shù)(如果有的話),否則不會(huì)有釋放內(nèi)存的動(dòng)作發(fā)生。這種策略很好地處理了標(biāo)準(zhǔn)庫中的使用指定內(nèi)存的new操作符,以及提供了成對的分配與釋放函數(shù)的任何情形。
看下面這個(gè)例子:
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];
}
分析:p1和p2將能正確釋放其分配的內(nèi)存,不會(huì)造成內(nèi)存泄漏,這屬于一種正常情況。后面的四種情況則比較復(fù)雜。對象a如果是采用普通方式分配的內(nèi)存,那么將能夠正確釋放其擁有的內(nèi)存。
7. malloc/free沒用了嗎?
new能夠完全替代malloc嗎?絕大部分情況下,答案都是肯定的。但是有一種情況則非用malloc不可了。根據(jù)new的定義,其第一個(gè)參數(shù)是待分配對象的大小,但在使用時(shí)不需要明確地給出這個(gè)值。這個(gè)值是由編譯器暗中替你完成的。倘若在某種情況下,需要在分配一個(gè)對象的同時(shí)還要分配出一些額外的空間用來管理某些相關(guān)的信息。這個(gè)額外空間與對象的空間要求連續(xù)。這個(gè)時(shí)候new就幫不上了。必須用malloc把對象和額外空間的總大小作為malloc的參數(shù)。在分配出來了后,可能需要調(diào)用new的放置形式的調(diào)用在該塊內(nèi)存上構(gòu)造對象。
8. 垃圾收集
當(dāng)我們?yōu)樽约旱念愄峁┝俗约旱膬?nèi)存管理方法時(shí),有可能會(huì)出現(xiàn)內(nèi)存分配失敗的情況。因此我們可能會(huì)通過set_new_handler()提供一個(gè)更靈巧的內(nèi)存釋放與重用機(jī)制。這就為實(shí)現(xiàn)垃圾收集提供了一個(gè)實(shí)現(xiàn)思路。垃圾收集機(jī)制的基本思想是,當(dāng)一個(gè)對象不再被引用時(shí),它的內(nèi)存就可以安全地被新的對象所使用。
當(dāng)比較垃圾收集機(jī)制與手工管理方式的代價(jià)時(shí),從一下幾個(gè)方面進(jìn)行比較:
運(yùn)行時(shí)間,內(nèi)存的使用,可靠性,移植性,編程的費(fèi)用,垃圾收集器的費(fèi)用,性能的預(yù)期。
垃圾收集器必須要處理幾個(gè)重要的問題:
(1)指針的偽裝
通常若以非指針的形式來存儲(chǔ)一個(gè)指針,則把這個(gè)指針叫做“偽裝的指針”。看下面這個(gè)例子:
void f()
{
int* p = new int;
long i1 = reinterpret_cast<long>(p) & 0xFFFF0000;
long i2 = reinterpret_cast<long>(p) & 0x0000FFFF;
p = 0;
// 這里就不存在指向那個(gè)整型數(shù)的指針了!
p = reinterpret_cast<int*>(i1|i2);
// 現(xiàn)在這個(gè)整型數(shù)又被引用了!
}
上例中原本由p持有的指針被偽裝成兩個(gè)整型數(shù)i1和i2。垃圾收集器必須關(guān)注這種偽裝的指針。
指針偽裝還有另外一種形式,即同時(shí)有指針和非指針成員的union結(jié)構(gòu),也會(huì)給垃圾收集器帶來特殊的問題。看下面的例子:
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中包含的是指針還是整數(shù)。
(2)delete函數(shù)
通常若使用了自動(dòng)垃圾收集,那么delete和delete[]函數(shù)是不再需要了的。但是delete和delete[]函數(shù)除了釋放內(nèi)存的功能外,還會(huì)調(diào)用析構(gòu)函數(shù)。因此在這種情況下,下列調(diào)用
delete p;
就只調(diào)用析構(gòu)函數(shù),而內(nèi)存的復(fù)用則向后推遲,直到內(nèi)存塊被收集。一次回收多個(gè)對象,有助于減少碎片。
(3)析構(gòu)函數(shù)
當(dāng)垃圾收集器準(zhǔn)備回收對象時(shí),有兩種辦法可選:
[1] 為這個(gè)對象調(diào)用析構(gòu)函數(shù)(如果有的話);
[2] 將這個(gè)對象當(dāng)作原始內(nèi)存(即不調(diào)用析構(gòu)函數(shù))。
一般垃圾收集器會(huì)選擇第二中方法。這種方法gc就成為模擬一種無限內(nèi)存的機(jī)制。
也有可能設(shè)計(jì)一種gc,它能調(diào)用向它注冊了的對象的析構(gòu)函數(shù)。這種設(shè)計(jì)的一個(gè)重要方面是防止析構(gòu)函數(shù)重復(fù)刪除一個(gè)之前已經(jīng)刪除的對象。
(4)內(nèi)存的碎片
處理內(nèi)存碎片的問題上,有兩種主要的GC類型:拷貝型和保守型。拷貝型GC通過移動(dòng)對象使得碎片空間緊湊;而保守型則通過分配方式的改善來減少碎片。C++的觀點(diǎn)看來,更傾向于保守型的。因?yàn)橐苿?dòng)對象將導(dǎo)致大量的指針和引用等失效,所以拷貝型GC在C++中幾乎是不可能實(shí)現(xiàn)的。此外保守型GC也可以讓C++的代碼段與C代碼段共存。