首頁 >後端開發 >C#.Net教程 >C++記憶體管理詳解

C++記憶體管理詳解

黄舟
黄舟原創
2016-12-16 09:50:571086瀏覽

1.對應的new和delete要採用相同的形式    下面的語句有什麼錯?
string *stringarray = new string[100];
...
delete stringarray;

一切好像都井然有序-一個new對應著一個delete-然而卻隱藏著很大的錯誤:程式的運作情況將是不可猜測的。至少,stringarray指向的100個string物件中的99個不會被正確地摧毀,因為他們的析構函數永遠不會被呼叫。

用new的時候會發生兩件事。首先,記憶體被分配(透過Operator new 函數,詳見條款7-10和條款m8),然後,為被分配的記憶體呼叫一個或多個建構函數。用delete的時候,也有兩件事發生:首先,為將被釋放的記憶體呼叫一個或多個析構函數,然後,釋放記憶體(透過operator delete 函數,詳見條款8和m8)。對於 delete來說會有這樣一個重要的問題:記憶體中有多少個物件要被刪除?答案決定了將有多少個析構函數會被呼叫。

這個問題簡單來說就是:要被刪除的指標指向的是單一物件呢,還是物件陣列?這只有你來告訴delete。假如你在用delete時沒用括號,delete就會認為指向的是單一對象,否則,它就會認為指向的是一個陣列:

string *stringptr1 = new string;
string *stringptr2 = new string[100 ];
...

delete stringptr1;// 刪除一個物件
delete [] stringptr2;// 刪除物件陣列

假如你在stringptr1前加了"[]"會怎麼樣呢?答案是:那會是不可猜測的;假如你沒在stringptr2前沒加上"[]"又會怎樣呢?答案也是:不可猜測。而且對於像int這樣的固定型別來說,結果也是不可猜測的,即使這樣的型別沒有析構函數。所以,解決這類問題的規則很簡單:假如你呼叫 new時用了[],呼叫delete時也要用[]。假如呼叫new時沒有用[],那呼叫delete時也不要用[]。

在寫一個包含指標資料成員,並且提供多個建構函式的類別時,牢記這項規則尤其重要。因為這樣的話,你就必須在所有初始化指標成員的建構子裡採用相同的new的形式。否則,析構函數裡將採用什麼形式的delete呢?關於此主題的進一步闡述,請參閱條款11。

這個規則對喜歡用typedef的人來說也很重要,因為寫typedef的程式設計師必須告訴別人,用new創建了一個typedef定義的類型的物件後,該用什麼形式的delete來刪除。舉例如下:

typedef string addresslines[4]; //一個人的位址,共4行,每行一個string
//因為addresslines是個數組,使用new:
string *pal = new addresslines; // 注重" new addresslines"返回string*, 和
// "new string[4]"返回的一樣
delete時必須以數組形式與之對應:
delete pal;// 錯誤!
delete [] pal;// 正確

為了避免混亂,最好杜絕對數組類型用typedefs。這其實很輕易,因為標準c++函式庫(見條款49)包含有stirng和vector模板,使用他們將會使對數組的需求減少到幾乎零。舉例來說,addresslines可以定義為一個字串(string)的向量(vector),即 addresslines可定義為vector類型。

2.析構函式裡對指標成員呼叫delete

大多數情況下,執行動態記憶體分配的的類別都在建構函式裡用new分配內存,然後在析構函式裡用delete釋放記憶體。最初寫這個類別的時候當然不難做,你會記得最後對在所有建構函式里分配了記憶體的所有成員使用delete。

然而,這個類別經過維護、升級後,情況就會變得困難了,因為對類別的程式碼進行修改的程式設計師不一定就是最早寫這個類別的人。而增加一個指標成員意味著幾乎都要進行下面的工作:
·在每個構造函數裡對指標進行初始化。對於一些建構函數,假如沒有記憶體要分配給指標的話,指標要被初始化為0(即空指標)。
·刪除現有的內存,透過賦值操作子分配給指標新的記憶體。
·在析構函數裡刪除指標。

假如在構造函數裡忘了初始化某個指針,或者在賦值操作的過程中忘了處理它,問題會出現得很快,很明顯,所以在實踐中這兩個問題不會那麼折磨你。但是,假如在析構函數裡沒有刪除指針,它不會表現出明顯的外部症狀。相反,它可能只是表現為一點微小的記憶體洩露,並且不斷增長,最後吞噬了你的位址空間,導致程式夭折。因為這種情況常常不那麼引人注重,所以每增加一個指針成員到類別裡時一定要記清楚。

另外,刪除空指標是安全的(因為它什麼也沒做)。所以,在寫建構函數,賦值操作符,或其他成員函數時,類別的每個指標成員要麼指向有效的內存,要麼就指向空,那在你的析構函數裡你就可以只用簡單地delete掉他們,不用擔心他們是不是被new過。

當然對本條款的使用也不要絕對。例如,你當然不會用delete去刪除一個沒有用new來初始化的指針,而且,就像用智能指針對象時不用勞你去刪除一樣,你也永遠不會去刪除一個傳遞給你的指針。換句話說,除非類別成員最初用了new,否則是不用在析構函數裡用delete的。

說到智慧指針,這裡介紹一種避免必須刪除指針成員的方法,即把這些成員用智能指針對象來代替,比如c++標準庫裡的auto_ptr。想知道它是如何運作的,看看條款m9和m10。

3.預先預備好內存不夠的情況
  operator new在無法完成內存分配請求時會拋出異常(以前的做法一般是返回0,一些舊一點的編譯器還這麼做。你願意的話也可以把異常(以前的做法是返回0,一些舊一點的編譯器還這麼做。你願意的話也可以把你願意的話也可以把你的編譯器設定成這樣。大家都知道,處理記憶體不夠所產生的異常真可以算是個道德上的行為,但實際做起來又會像刀架在脖子上那樣痛苦。所以,你有時會不去管它,也許一直沒去管它。但你心裡一定還是深深隱藏著一種罪惡感:萬一new真的產生了異常怎麼辦?
你會很自然地想到處理這種情況的一種方法,即回到以前的老路上去,使用預處理。例如,c的一種常用的做法是,定義一個類型無關的巨集來分配記憶體並檢查分配是否成功。對c++來說,這個巨集看起來可能像這樣:


#define new(ptr, type) 
try { (ptr) = new type; } 
catch (std::bad_alloc&) { assert(0);

(「慢!std::bad_alloc是做什麼的?」你會問。bad_alloc是operator new不能滿足記憶體分配請求時拋出的例外類型,std是bad_alloc所在的名字空間(見條款28)的名稱。巨集。 。

new宏不但有著上面所說的通病,即用assert去檢查可能發生在已發布程序裡的狀態(然而任何時候都可能發生內存不夠的情況),同時,它還在c ++裡有另外一個缺陷:它沒有考慮到new有各種各樣的使用方式。例如,想建立類型t對象,一般有三種常見的語法形式,你必須對每種形式可能產生的異常都要處理:


new t;
new t(constrUCtor arguments);
new t[size ];

這裡對問題大大進行了簡化,因為有人還會自訂(重載)operator new,所以程式裡會包含任意個使用new的語法形式。

那麼,怎麼辦?假如想用一個很簡單的出錯處理方法,可以這麼做:當記憶體分配請求不能滿足時,呼叫你預先指定的一個出錯處理函數。這個方法是基於一個常規,即當operator new無法滿足請求時,會在拋出例外之前呼叫客戶指定的一個出錯處理函數-一般稱為new-handler函數。 (operator new實際運作起來要複雜一些,詳見條款8)

指定出錯處理函數時要用到set_new_handler函數,它在頭文件裡大致是像下面這樣定義的:


typedef void (*new_handler) ();
new_handler set_new_handler(new_handler p) throw();

可以看到,new_handler是一個自訂的函數指標類型,它指向一個沒有輸入參數也沒有傳回值的函數。 set_new_handler則是一個輸入並傳回new_handler類型的函數。

set_new_handler的輸入參數是operator new分配內存失敗時要調用的出錯處理函數的指針,返回值是set_new_handler沒調用之前就已經在起作用的舊的出錯處理函數的指針。

可以像下面這樣使用set_new_handler:


// function to call if operator new can't allocate enough memory
void nomorememory()
{at for
cer );
}

int main()
{
set_new_handler(nomorememory);
int *pbigdataarray = new int[100000000];

...
}
}
}假如operator new不能為100,000,000個整數分配空間,nomorememory將會被調用,程式發出一條出錯訊息後終止。這就比簡單地讓系統核心產生錯誤訊息來結束程式要好。 (順便考慮一下,如果cerr在寫錯誤訊息的過程中要動態分配內存,那將會發生什麼...)

operator new不能滿足內存分配請求時,new-handler函數不只調用一次,而是不斷重複,直至找到足夠的記憶體。實作重複呼叫的程式碼在條款8裡可以看到,這裡我用描述性的語言來說明:一個設計得好的new-handler函數必須實作下面函數中的一種。
·產生更多的可用記憶體。這將使operator new下一次分配記憶體的嘗試有可能獲得成功。實作此策略的一個方法是:在程式啟動時分配一個大的記憶體區塊,然後在第一次呼叫new-handler時釋放。釋放時伴隨著一些對使用者的警告訊息,如記憶體數量太少,下次請求可能會失敗,除非又有更多可用的空間。
·安裝另一個不同的new-handler函數。假如當前的new-handler函數不能產生更多的可用內存,可能它會知道另一個new- handler函數可以提供更多的資源。這樣的話,目前的new-handler可以安裝另一個new-handler來取代它(透過呼叫 set_new_handler)。下一次operator new呼叫new-handler時,會使用最近安裝的那個。 (這個策略的另一個變通辦法是讓new-handler可以改變它自己的運行行為,那麼下次呼叫時,它將做不同的事。方法是使new-handler可以修改那些影響它自身行為的靜態或全域資料。也就是傳遞空指標給set_new_handler。沒有安裝new-handler,operator new分配記憶體不成功時就會拋出一個標準的std::bad_alloc類型的例外。
·拋出std::bad_alloc或從std::bad_alloc繼續的其他類型的例外。這樣的異常不會被operator new捕捉,所以它們會被送到最初進行記憶體請求的地方。 (拋出別的不同類型的異常會違反operator new異常規範。規範中的缺省行為是調用abort,所以new-handler要拋出一個異常時,一定要確信它是從std::bad_alloc繼續來的。典型做法是呼叫abort或exit。 abort/exit可以在標準c函式庫中找到(還有標準c++函式庫,參見條款49)。

上面的選擇給了你實現new-handler函數極大的靈活性。

處理記憶體分配失敗的情況時採取什麼方法,取決於要分配的物件的類別:


class x {
public:
static void

outofmemory();

...
}
}
}
class y {
public:
static void outofmemory();

...

};

x* p1 = new x; // 若分配成功,呼叫x::outofmemory
x* p1 = new x; // 若分配成功,呼叫x::outofmemory
; // 若分配不成功,呼叫y::outofmemory

c++不支援專門針對於類別的new-handler函數,而且也不需要。你可以自己來實作它,只要在每個類別中提供自己版本的set_new_handler和operator new。類別的set_new_handler可以為類別指定new-handler(就像標準的set_new_handler指定全域new-handler一樣)。類別的operator new則保證為類別的物件分配記憶體時用類別的new-handler取代全域new-handler。

假設處理類別x記憶體分配失敗的情況。因為operator new對類型x的物件分配記憶體失敗時,每次都必須呼叫出錯處理函數,所以要在類別裡宣告一個new_handler類型的靜態成員。那麼類別x看起來會像這樣:


class x {
public:
static new_handler set_new_handler(new_handler p);
static void * operator new(size_t size);
);

類別的靜態成員必須在類別外定義。因為想藉用靜態物件的預設初始化值0,所以定義x::currenthandler時沒有去初始化。


new_handler x::currenthandler; //缺省設定currenthandler為0(即null)
類別x中的set_new_handler函數會儲存傳給它的任何指針,並傳回在呼叫它之前所儲存的任何指標。這正是標準版本的set_new_handler所做的:


new_handler x::set_new_handler(new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler; new所做的:
1. 呼叫標準set_new_handler函數,輸入參數為x的出錯處理函數。這使得x的new-handler函數成為全域new-handler函數。注重下面的程式碼中,用了"::"符號明確地引用std空間(標準set_new_handler函數就存在於std空間)。

2. 呼叫全域operator new分配記憶體。假如第一次分配失敗,全域operator new會呼叫x的new-handler,因為它剛剛(見1.)被安裝成為全域new-handler。假如全域operator new最終未能分配到內存,它拋出std::bad_alloc異常,x的operator new會捕捉到它。 x的operator new然後恢復最初被取代的全域new-handler函數,最後以拋出異常返回。

3. 假設全域operator new為類型x的物件分配記憶體成功,, x的operator new會再次呼叫標準set_new_handler來恢復最初的全域出錯處理函數。最後傳回分配成功的記憶體的指標。
c++是這麼做的:


void * x::operator new(size_t size)
{
new_handler globalhandler = // 安裝x的new_handler
std::set_new_handler(currentler); try { // 嘗試分配記憶體
memory = ::operator new(size);
}

catch (std::bad_alloc&) { // 恢復舊的new_handler
std::set_new_handler(globalhandler);拋出例外
}
std::set_new_handler(globalhandler); // 恢復舊的new_handler
return memory;
}

假如你對上面重複調用std::set_new_handlerm順眼,可以參見條款?

使用類別x的記憶體分配處理功能時大致如下:


void nomorememory();// x的物件分配記憶體失敗時呼叫的new_handler函數的宣告

x::set_new_handler(nomorememory);把nomorememory設定為x的
// new-handling函數
x *px1 = new x;
// 如記憶體分配失敗,
// 呼叫nomorememory
string *ps = new string;
// 如記憶體分配失敗,呼叫全域new-handling函數

x::set_new_handler(0);
// 設x的new-handling函數為空

x *px2 = new x;
// 如記憶體分配失敗,立即拋出異常
// (類別x沒有new-handling函數)

你會注重到,處理以上類似情況,假如不考慮類的話,實現代碼是一樣的,這就很自然地想到在別的地方也能重用它們。如條款41所說明的,繼續和範本可以用來設計可重複使用程式碼。在這裡,我們把兩種方法結合起來使用,從而滿足了你的要求。

你只要建立一個「混合風格」(mixin-style)的基類,這種基類答應子類繼續它某一特定的功能-這裡指的是建立一個類的new-handler的功能。之所以設計一個基底類,是為了讓所有的子類別可以繼續set_new_handler和operator new功能,而設計模板是為了讓每個子類別有不同的currenthandler資料成員。這聽起來很複雜,不過你會看到程式碼其實很熟悉。區別只不過是它現在可以被任何類別重用了。


template // 提供類別set_new_handler支援的
class newhandlersupport { // 混合風格」的基底類別
public:
static new_handler set_new_handler(new_handler p);
static new_handler set_new_handler(new_handler p);
static new_handler set_new_handler(new_handler p);
static new_handler set_new_handler(new_handler p); :
static new_handler currenthandler;
};

template
new_handler newhandlersupport::set_new_handler(new_handler p)
{
new_handler oldhandler = currentler; late
void * newhandlersupport:: operator new(size_t size)
{
new_handler globalhandler =
std::set_new_handler(currenthandler);
void *memory;
try {
memory = ::operator new(size);
}::
memory = ::operator new(size); {
std::set_new_handler(globalhandler);
throw;
}

std::set_new_handler(globalhandler);
return memory;
}
//this sets sup currentler ler;
有了這個模板類,對類x加上set_new_handler功能就很簡單了:只要讓x從newhandlersupport繼續:
// note inheritance from mixin base class template. (see
// my article on counting objects for informee
// my article on counting objects for information on why
// private inheritance might be preferable here.)
class x: public newhandlersupport {

... // as before, but no declarations for
}; // set_new_handler or operator new
};不用理會它幕後在做些什麼;舊代碼依然工作。這很好!那些你常常不去理會的東西往往是最可信賴的。

使用set_new_handler是處理記憶體不夠用下一種方便,簡單的方法。這比把每個new都包裝在try模組裡當然好多了。而且, newhandlersupport這樣的模板使得向任何類別增加一個特定的new-handler變得更簡單。 「混合風格」的持續不可避免地將話題引入到多繼續上去,在轉到這個主題之前,你一定要先閱讀條款43。

1993年前,c++一直要求在記憶體分配失敗時operator new要回傳0,現在則是要求operator new拋出std::bad_alloc異常。很多c++程式是在編譯器開始支援新規範前寫的。 c++標準委員會不想放棄那些已有的遵循返回0規範的程式碼,所以他們提供了另外形式的operator new(以及operator new[]——請參閱條款8)以繼續提供返回0功能。這些形式被稱為“無拋出”,因為他們沒用過一個throw,而是在使用new的入口點採用了nothrow物件:


class widget { ... };

widget *pw1 = new widget;// 分配失敗拋出std::bad_alloc if

if (pw1 == 0) ... // 這個檢查一定失敗
widget *pw2 = new (nothrow) widget; // 若分配失敗回傳0

if (pw2 == 0) ... // 這個檢查可能會成功

不管是用「正規」(即拋出異常)形式的new還是「無拋出」形式的new,重要的是你必須為記憶體分配失敗做好預備。最簡單的方法是使用set_new_handler,因為它對兩種形式都有用。

4.寫operator new和operator delete時要遵循常規

自己重寫operator new時(條款10解釋了為什麼有時要重寫它),很重要的一點是函數提供的行為要和系統缺省的operator new一致。實際做起來也就是:要有正確的回傳值;可用記憶體不夠時要呼叫出錯處理函數(見條款7);處理好0位元組記憶體請求的情況。此外,也要避免不小心隱藏了標準形式的new,不過這是條款9的話題。

有關傳回值的部分很簡單。如果記憶體分配請求成功,就傳回指向記憶體的指標;假如失敗,則遵循條款7的規定拋出一個std::bad_alloc類型的例外。

但事情也不是那麼簡單。因為operator new實際上會不止一次地嘗試著去分配內存,它要在每次失敗後調用出錯處理函數,還期望出錯處理函數能想辦法釋放別處的內存。只有在指向出錯處理函數的指標為空的情況下,operator new才會拋出例外。

另外,c++標準要求,即使在請求分配0位元組記憶體時,operator new也要回傳一個合法指標。 (實際上,這個聽起來怪怪的要求確實給c++語言其它地方帶來了簡便)

這樣,非類成員形式的operator new的偽代碼看起來會像下面這樣:
void * operator new(size_t size) // operator new也可能有其它參數
{

if (size == 0) { // 處理0字節請求時,
size = 1; // 把它當作1個位元組請求來處理
}
while (1) {
分配size位元組記憶體;

if (分配成功)
return (指向記憶體的指標);

// 分配不成功,找出目前出錯處理函數
new_handler global_ler (0);
set_new_handler(globalhandler);

if (globalhandler) (*globalhandler)();
else throw std::bad_alloc();
}
}

在於處理零字節的技巧請求一個位元組來處理。這看起來也很怪,但簡單,合法,有效。而且,你又會多久碰到一次零位元組請求的情況呢?

你又會希奇上面的偽代碼中為什麼把出錯處理函數置為0後又立即恢復。這是因為沒有辦法可以直接得到出錯處理函數的指針,所以必須透過呼叫set_new_handler來找到。辦法很笨但也有效。


條款7提到operator new內部包含一個無限循環,上面的程式碼清楚地說明了這一點-while (1)將導致無限循環。跳出迴圈的唯一辦法是記憶體分配成功或出錯處理函數完成了條款7所描述的事件中的一種:得到了更多的可用記憶體;安裝了一個新的new -handler(出錯處理函數);卸除了new-handler;拋出了一個std::bad_alloc或其派生類型的例外;或傳回失敗。現在明白了為什麼new-handler必須做這些工作中的一件。假如不做,operator new裡面的循環就不會結束。

很多人沒有熟悉到的一點是operator new經常會被子類別繼續。這會導致某些複雜性。上面的偽代碼中,函數會去分配size位元組的記憶體(除非size為0)。 size很重要,因為它是傳遞給函數的參數。但大多數針對類別所寫的operator new(包括條款10中的那種)都是只為特定的類別設計的,不是為所有的類,也不是為它所有的子類別設計的。這意味著,對於一個類別x的operator new來說,函數內部的行為在涉及到物件的大小時,都是精確的sizeof(x):不會大也不會小。但由於存在繼續,基底類別中的operator new可能會被呼叫去為一個子類別物件分配記憶體:
class base {
public:
static void * operator new(size_t size);
...
};

class derived: public base // derived類別沒有宣告operator new
{ ... }; //

derived *p = new derived; // 呼叫base::operator new

假如base類的operator new不想費功夫專門去處理這種情況——這種情況出現的可能性不大——那最簡單的辦法就是把這個「錯誤」數量的記憶體分配請求轉給標準operator new來處理,像下面這樣:
void * base::operator new(size_t size)
{
if (size != sizeof(base)) // 假如數量“錯誤”,讓標準operator new
return ::operator new( size); // 去處理這個請求
//

... // 否則處理這個請求
}

「停!」我聽見你在叫,「你忘了檢查一種雖然不合理但是有可能出現的一種情況——size有可能為零!」是的,我沒檢查,但拜託下次再叫出聲的時候不要這麼文縐縐的。 :)但實際上檢查還是做了,只不過融合到size != sizeof(base)語句中了。 c++標準很怪異,其中之一就是規定所以獨立的(freestanding)類別的大小都是非零值。所以sizeof(base)永遠不可能是零(即使base類別沒有成員),假如size為零,請求會轉到::operator new,由它來以一種合理的方式對請求進行處理。 (有趣的是,假如base不是獨立的類,sizeof(base)有可能是零,具體說明參見"my article on counting objects")。

假如想控制基於類別的陣列的記憶體分配,必須實作operator new的陣列形式-operator new[](這個函數常被稱為“陣列new”,因為想不出"operator new[]")該怎麼發音)。寫operator new[]時,要記住你面對的是「原始」內存,不能對數組裡還不存在的物件進行任何操作。實際上,你甚至還不知道數組裡有多少個對象,因為你不知道每個對像有多大。基底類別的operator new[]會透過繼續的方式被用來為子類別物件的數組分配內存,而子類別物件往往比基底類別要大。所以,不能想當然認為base::operator new[]裡的每個物件的大小都是sizeof(base),也就是說,陣列裡物件的數量不一定就是(請求位元組數)/sizeof(base)。關於operator new[]的具體介紹參見條款m8。

重寫operator new(和operator new[])時所有要遵循的常規就這些。對於operator delete(以及它的夥伴operator delete[]),情況更簡單。要記住的只是,c++保證刪除空指標永遠是安全的,所以你要充分地應用這項保證。以下是非類別成員形式的operator delete的偽代碼:
void operator delete(void *rawmemory)
{
if (rawmemory == 0) return; file://如/果指針為空,回傳
//

釋放rawmemory指向的記憶體;

return;
}

這個函數的類別成員版本也簡單,只是還必須檢查被刪除的物件的大小。假設類別的operator new將「錯誤」大小的分配請求轉給::operator new,那麼也必須將「錯誤」大小的刪除請求轉給::operator delete:

class base { // 和前面一樣,只是這裡宣告了
public: // operator delete
static void * operator new(size_t size);
static void operator delete(void *rawmemory, size_t size);
}
}; (void *rawmemory, size_t size)
{
if (rawmemory == 0) return; // 檢查空指標

if (size != sizeof(base)) { // 假如size"錯誤",
::operator delete(rawmemory); // 讓標準operator來處理請求
return;
}

釋放指向rawmemory的記憶體;

return;
}

數可見,有關operator new和operator delete(以及他們的陣列形式可見,有關operator new和operator delete(以及他們的陣列形式可見,有關operator new和operator delete(規定不是那麼麻煩,重要的是必須遵守它。只要記憶體分配程序支援new-handler函數並正確地處理了零記憶體請求,就差不多了;假如記憶體釋放程序又處理了空指針,那就沒其他什麼要做的了。至於在類別成員版本的函數裡增加繼續支持,那將很快就可以完成。
5.避免隱藏標準形式的new           
 因為內部範圍聲明的名稱會隱藏掉外部範圍的相同的名稱,所以對於分別在類別的內部
和具有相同名字的全域聲明的兩個名稱成員函數會隱藏掉全域函數:
void f(); // 全域函數
class x {
public:
void f(); // 成員函數
};

x x;

f); //呼叫f

x.f(); // 呼叫x::f

這不會令人驚奇,也不會導致混淆,因為調用全域函數和成員函數時總是採用不同的

語法形式。然而假如你在類別裡增加了一個帶有多個參數的operator new函數,結果就有

可能令人大吃一驚。

class x {
public:
void f();

// operator new的參數指定一個
// new-hander(new的出錯處理)函數
static void * operator new(size_handt p. ;
};

void specialerrorhandler(); // 定義在別的地方

x *px1 =
new (specialerrorhandler) x; // 呼叫x::operator new

x *px2 = new x;錯誤!

在類別裡定義了一個稱為「operator new」的函數後,會不經意地阻止了對標準new的訪

問。條款50解釋了為什麼會這樣,這裡我們更關心的是如何想個辦法避免這個問題。

一個辦法是在類別裡寫一個支援標準new呼叫方式的operator new,它和標準new做同樣

的事。這可以用一個高效的內聯函數來封裝實作。

class x {
public:
void f();

static void * operator new(size_t size, new_handler p);

static void * operator new(size_handler p);

static void * operator new(size_handler p);

static void* operator new(size_pize))
* ; }
};

x *px1 =
new (specialerrorhandler) x; // 呼叫x::operator
// new(size_t, new_handler)

x* px2 = new x;
// new(size_t)

另一種方法是為每一個增加到operator new的參數提供缺省值(見條款24):

class x {
public:
void f();

class x {
public:
void f();

static
void * operator new(size_t size, // p預設值為0
new_handler p = 0); //
};

x *px1 = new (specialerrorhandler) x; // 正確
x *px1 = new (specialerrorhandler) x; // 正確
x
x new x; // 也正確

無論哪種方法,假如以後想對「標準」形式的new定制新的功能,只需要重寫這個函數。

呼叫者重新編譯連結後就可以使用新功能了。

6. 假如寫了operator new就要同時寫operator delete

讓我們回過頭去看看這樣一個基本問題:為什麼有必要寫自己的operator new和operator delete?

答案通常是:為了效率。缺省的operator new和operator delete具有非常好的通用性,它的這種靈活性也使得在某些特定的場合下,可以進一步改善它的性能。尤其在那些需要動態分配大量的但很小的物件的應用程式裡,情況更是如此。

例如有這樣一個表示飛機的類別:類別airplane只包含一個指針,它指向的是飛機物件的實際描述(此技術在條款34進行說明):

class airplanerep { ... }; // 表示一個飛機物件
//
class airplane {
public:
...
private:
airplanerep *rep; // 指向實際描述
};

一個airplane物件並不大,它只包含一個指標(正如條款14和m24所說明的,假如airplane類別聲明了虛函數,會隱式包含第二個指標)。但當呼叫operator new來分配一個airplane物件時,得到的記憶體可能要比儲存這個指標(或一對指標)所需要的還要多。之所以會產生這種看起來很希奇的行為,在於operator new和operator delete之間需要互相傳遞訊息。

因為預設版本的operator new是一種通用型的記憶體分配器,它必須可以分配任意大小的記憶體區塊。同樣,operator delete也要可以釋放任意大小的記憶體區塊。 operator delete想弄清楚它要釋放的記憶體有多大,就必須知道當初operator new分配的記憶體有多大。有一種常用的方法可以讓operator new來告訴operator delete當初分配的內存大小是多少,就是在它所返回的內存裡預先附帶一些額外信息,用來指明被分配的內存塊的大小。也就是說,當你寫了下面的語句,

airplane *pa = new airplane;

你不會得到一塊看起來像這樣的記憶體區塊:

pa——> airplane物件的記憶體

而是得到像這樣的記憶體區塊:

pa——> 記憶體區塊大小資料+ airplane物件的記憶體

對於象airplane這樣很小的物件來說,這些額外的資料資訊會使得動態分配物件時所需要的的記憶體的大小倍增(非凡是類別裡沒有虛擬函數的時候)。

假如軟體運行在一個記憶體很寶貴的環境中,就承受不起這種奢侈的記憶體分配方案了。為airplane類別專門寫一個operator new,就可以利用每個airplane的大小都相等的特點,不必在每個分配的記憶體區塊上加上附帶資訊了。

具體來說,有這樣一個方法來實現你的自訂的operator new:先讓缺省operator new分配一些大塊的原始內存,每塊的大小都足以容納很多個airplane物件。 airplane物件的記憶體區塊就取自這些大的記憶體區塊。目前沒被使用的記憶體區塊被組織成鍊錶-稱為自由鍊錶-以備未來airplane使用。聽起來好像每個物件都要承擔一個next域的開銷(用於支援鍊錶),但不會:rep 域的空間也被用來儲存next指標(因為只是作為airplane物件來使用的記憶體區塊才需要rep指標;同樣,只有沒作為airplane物件使用的記憶體區塊才需要next指標),這可以用union來實作。


具體實作時,就要修改airplane的定義,從而支援自訂的記憶體治理。可以這麼做:

class airplane { // 修改後的類別— 支援自訂的記憶體治理🎜public: //🎜🎜static void * operator new(size_t size);🎜🎜...🎜private: {🎜airplanerep *rep; // 用於被使用的物件
airplane *next; // 用於沒被使用的(在自由鍊錶中)物件
};

// 類別的常數,指定一個大的記憶體區塊中放多少個
// airplane對象,在後面初始化
static const int block_size;

static airplane *headoffreelist;

};

上面的程式碼增加了的幾個聲明:一個聯合operator new),一個聯合(使得上面的程式碼增加了的幾個聲明:一個聯合)operator rep和next域佔用同樣的空間),一個常數(指定大記憶體區塊的大小),一個靜態指標(追蹤自由鍊錶的表頭)。表頭指標宣告為靜態成員很重要,因為整個類別只有一個自由鍊錶,而不是每個airplane物件都有。

下面該寫operator new函數了:

void * airplane::operator new(size_t size)
{
// 把「錯誤」大小的請求轉給::operator new()處理;
// 詳見條款8
if (size != sizeof(airplane))
return ::operator new(size);

airplane *p = // p指向自由鍊錶的表頭
headoffreelist; //

// p ,則將表頭移到它的下一個元素
//
if (p)
headoffreelist = p->next;

else {
// 自由鍊錶為空,則分配一個大的記憶體區塊,
/ / 可以容納block_size個airplane物件
airplane *newblock =
static_cast(::operator new(block_size *
sizeof(airplane)));

// 將每個小記憶體區塊連結起來形成新的自由鍊錶
/ / 跳過第0個元素,因為它要回傳給operator new的呼叫者
//
for (int i = 1; i newblock[i].next = &newblock[ i+1];

// 用空指標結束鍊錶
newblock[block_size-1].next = 0;

// p 設為表的頭部,headoffreelist指向的
// 內存塊緊跟其後
p = newblock;
headoffreelist = &newblock[1];
}

return p;
}

假如你讀了條款8,就會知道在operator new不能滿足內存分配請求時,會執行一系列內存分配請求時,會執行一系列內存與new -handler函數和例外有關的例行動作。上面的程式碼沒有這些步驟,這是因為operator new治理的記憶體都是從::operator new分配來的。這表示只有::operator new失敗時,operator new才會失敗。而假如::operator new失敗,它會去執行new-handler的動作(可能最後以拋出異常結束),所以不需要airplane的operator new也去處理。換句話說,其實new-handler的動作都還在,你只是沒看見,它隱藏在::operator new裡。

有了operator new,下面要做的就是給出airplane的靜態資料成員的定義:

airplane *airplane::headoffreelist;

const int airplane::block_size = 512;
const int airplane::block_size = 512;
const int airplane::block_size = 512;
headoffreelist設定為空指針,因為靜態成員的初始值都被預設設為0。 block_size決定了要從::operator new獲得多大的記憶體區塊。

這個版本的operator new將會運作得很好。它為airplane物件分配的記憶體比缺省operator new更少,而且運行得更快,可能會快2次方的等級。這沒什麼希奇的,通用型的缺省operator new必須應付各種大小的內存請求,還要處理內部外部的碎片;而你的operator new只用操作鍊錶中的一對指針。拋棄彈性往往可以輕易地換來速度。

下面我們將討論operator delete。還記得operator delete嗎?本條款就是關於operator delete的討論。但直到現在,airplane類別只聲明了operator new,還沒聲明operator delete。想想假如寫了下面的程式碼會發生什麼事:

airplane *pa = new airplane; // 呼叫
// airplane::operator new
...

delete pa; // ::operator delete
...

delete pa; // ::operator delete
...

delete pa; // ::operator delete
...

delete pa; // ::operator delete讀這段程式碼時,假如你豎起耳朵,會聽到飛機撞毀燃燒的聲音,還有程式設計師的哭泣。問題出在operator new(在airplane裡定義的那個)返回了一個不帶頭信息的內存的指針,而operator delete(缺省的那個)卻假設傳給它的內存包含頭信息。這就是悲劇產生的原因。

這個例子說明了一個普遍原則:operator new和operator delete必須同時寫,這樣才不會出現不同的假設。假如寫了一個自己的記憶體分配程序,就要同時寫一個釋放程序。 (為什麼要遵循這條規定的另一個理由,請參閱article on counting objects一文的the sidebar on placement章節)

因而,繼續設計airplane類別如下:

class airplane { // 和前面的一樣,只不過增加了一個
public: // operator delete的聲明
...

static void operator delete(void *deadobject,
size_t size);

delete;

/塊 傳給一個記憶體的記憶體, delete;假如
// 其大小正確,就加到自由內存塊鍊錶的最前面🎜//🎜void airplane::operator delete(void *deadobject,🎜🎜size_t size)🎜{🎜if (🎜🎜size_t size)🎜{🎜if (deadobject == == == == ==. // 見條款8🎜🎜if (size != sizeof(airplane)) { // 見條款8
::operator delete(deadobject);
return;
}

airplane *carcass =
static_cast(deadobject); ;
headoffreelist = carcass;
}

因為前面在operator new裡將「錯誤」大小的請求轉給了全域operator new(見條款8),那麼這裡同樣要將「錯誤」大小的物件交給全域operator delete來處理。假如不這樣,就會重現你前面費盡心思想避免的那種問題──new和delete句法上的不匹配。

有趣的是,假如要刪除的物件是從一個沒有虛析構函數的類別繼續而來的,那傳給operator delete的size_t值有可能不正確。這就是必須確保基類必須要有虛析構函數的原因,此外條款14也列出了第二個、理由更充足的原因。這裡只要簡單地記住,基底類別假如遺漏了虛擬構函數,operator delete就有可能無法運作正確。

所有一切都很好,但從你皺起的眉頭我可以知道你一定在擔心內存洩漏。有著大量開發經驗的你不會沒注重到,airplane的operator new呼叫::operator new 得到了大塊內存,但airplane的operator delete卻沒有釋放它們。內存外洩!內存外洩!我分明聽見了警鐘在你腦海裡迴響。

但請仔細聽我回答,這裡沒有記憶體洩漏!

造成記憶體外洩的原因在於記憶體分配後指向記憶體的指標遺失了。假如沒有垃圾處理或其他語言以外的機制,這些記憶體就不會被收回。但上面的設計沒有記憶體洩露,因為它絕不會出現記憶體指標遺失的情況。每個大記憶體區塊首先被分成airplane大小的小塊,然後這些小塊被放在自由鍊錶上。當客戶呼叫 airplane::operator new時,小塊被自由鍊錶移除,客戶得到指向小塊的指標。當客戶呼叫operator delete時,小塊被放回自由鍊錶上。採用這種設計,所有的記憶體區塊要不被airplane物件使用(這種情況下,是由客戶來負責避免記憶體外洩),要不要就在自由鍊錶上(這種情況下記憶體區塊有指標)。所以說這裡沒有記憶體外洩。

然而確實,::operator new回傳的記憶體區塊是從來沒有被airplane::operator delete釋放,這個記憶體區塊有個名字,叫做記憶體池。但記憶體洩漏和記憶體池有一個重要的不同之處。記憶體洩漏會無限地成長,即使客戶循規蹈矩;而記憶體池的大小絕不會超過客戶請求記憶體的最大值。

修改airplane的記憶體治理程式使得::operator new返回的記憶體區塊在不被使用時自動釋放並不難,但這裡不會這麼做,這有兩個原因:第一個原因和你自訂記憶體治理的初衷有關。你有很多理由去自訂記憶體治理,最基本的一條是你確認缺省的operator new和operator delete使用了太多的記憶體或(並且)運行很慢。和採用記憶體池策略相比,追蹤和釋放那些大記憶體區塊所寫的每一個額外的位元組和每一個額外的語句都會導致軟體運行更慢,用的記憶體更多。在設計效能要求很高的函式庫或程式時,假如你預期記憶體池的大小會在一個合理的範圍之內,那採用記憶體池的方法再好不過了。

第二個原因和處理一些不合理的程序行為有關。假設airplane的記憶體治理程式被修改了,airplane的operator delete可以釋放任何沒有物件存在的大塊的記憶體。那看下面的程式: int main()
{
airplane *pa = new airplane; // 第一次分配: 得到大塊內存,
// 生成自由鍊錶,等

delete pa; // 內存塊空;
// 釋放它

pa = new airplane; // 再次得到大塊內存,
// 生成自由鍊錶,等

delete pa; // 內存塊再次空,
// 釋放

... // 你有了想法...

return 0;
}

這個糟糕的小程式會比用缺省的operator new和operator delete寫的程式運行得還慢,佔用還要多的內存,更不要和用記憶體池寫的程式比了。

當然有辦法處理這種不合理的情況,但考慮的非凡情況越多,就越有可能要重新實現記憶體治理函數,而最後你又會得到什麼呢?記憶體池不能解決所有的記憶體治理問題,在很多情況下是很適合的。

實際開發中,你會經常要給許多不同的類別實現基於記憶體池的功能。你會想,「一定有什麼辦法把這種固定大小記憶體的分配器封裝起來,從而可以方便使用」。是的,有辦法。雖然我在這個條款已經嘮叨這麼久了,但還是要簡單介紹一下,具體實作留給讀者做練習。

下面簡單給出了一個pool類別的最小介面(見條款18),pool類別的每個物件是某類別物件(其大小在pool的建構子裡指定)的記憶體分配器。

class pool {
public:
pool(size_t n); // 為大小為n的物件建立
// 一個分配器


void * alloc(size_t n) ; // 為一個物件分配足夠記憶體
// 遵循條款8的operator new常規

void free( void *p, size_t n); // 將p所指的記憶體回到記憶體池;
// 遵循條款8的operator delete常規

~pool(); // 釋放記憶體池中全部記憶體

};

這個類別支援pool物件的創建,執行分配和釋放操作,以及被摧毀。 pool物件被摧毀時,會釋放它所分配的所有記憶體。這就是說,現在有辦法避免 airplane的函數裡所表現的記憶體洩漏似的行為了。然而這也意味著,假如pool的析構函數呼叫太快(使用記憶體池的物件沒有全部被摧毀),有些物件就會發現它正在使用的記憶體猛然間沒了。這造成的結果通常是不可猜測的。

有了這個pool類,即使java程式設計師也可以不費吹灰之力地在airplane類別裡增加自己的記憶體治理功能:

class airplane {
public:

... 普通airplane

static void * operator new(size_t size);
static void operator delete(void *p, size_t size);


private:
airplanerep *repempool; // 指向實際描述的指標
private:
airplanerep *repempool;池

};

inline void * airplane::operator new(size_t size)
{ return mempool.alloc(size); }

inline void airplane::operator delete(pize, p mempool.free(p, size); }

// 為airplane物件建立一個記憶體池,
// 在類別的實作檔案裡實作
pool airplane::mempool(sizeof(airplane));

這個設計比前面的要清楚、乾淨得多,因為airplane類別不再和非airplane的程式碼混在一起。 union,自由鍊錶頭指針,定義原始記憶體區塊大小的常數都不見了,它們都隱藏在它們應該待的地方-pool類別裡。讓寫pool的程式設計師去操心記憶體治理的細節吧,你的工作只是讓airplane類正常工作。

現在應該明白了,自訂的記憶體治理程式可以很好地改善程式的效能,而且它們可以封裝在像pool這樣的類別裡。但請不要忘記主要的一點,operator new和operator delete需要同時工作,那麼你寫了operator new,就也一定要寫operator delete。

以上就是C++記憶體管理詳解的內容,更多相關文章請關注PHP中文網(www.php.cn)!


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
上一篇:makefile規則下一篇:makefile規則