C 記憶體管理詳解

2021-12-29 22:31:29 字數 4970 閱讀 4212

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,一些舊一點的編譯器還這麼做。你願意的話也可以把你的編譯器設定成這樣。關於這個話題我將推遲到本條款的結尾處討論)。大家都知道,處理記憶體不夠所產生的異常真可以算得上是個道德上的行為,但實際做起來又會象刀架在脖子上那樣痛苦。所以,你有時會不去管它,也許一直沒去管它。但你心裡一定還是深深地隱藏著一種罪惡感:萬一new真的產生了異常怎麼辦?

你會很自然地想到處理這種情況的一種方法,即回到以前的老路上去,使用預處理。例如,c的一種常用的做法是,定義乙個型別無關的巨集來分配記憶體並檢查分配是否成功。對於c++來說,這個巨集看起來可能象這樣:

#define new(ptr, type)

try

catch (std::bad_alloc&)

(「慢!std::bad_alloc是做什麼的?」你會問。bad_alloc是operator new不能滿足記憶體分配請求時丟擲的異常型別,std是bad_alloc所在的名字空間(見條款28)的名稱。「好!」你會繼續問,「assert又有什麼用?」假如你看看標準c標頭檔案(或與它相等價的用到了名字空間的版本,見條款49),就會發現assert是個巨集。這個巨集檢查傳給它的表示式是否非零,假如不是非零值,就會發出一條出錯資訊並呼叫abort。assert只是在沒定義標準巨集ndebug的時候,即在除錯狀態下才這麼做。在產品發布狀態下,即定義了ndebug的時候,assert什麼也不做,相當於一條空語句。所以你只能在除錯時才能檢查斷言(assertion))。

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 cant allocate enough memory

void nomorememory()

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時,會使

C 記憶體管理詳解

偉大的bill gates 曾經失言 640k ought to be enough for everybody bill gates 1981 程式設計師們經常編寫記憶體管理程式,往往提心吊膽。如果不想觸雷,唯一的解決辦法就是發現所有潛伏的地雷並且排除它們,躲是躲不了的。本文的內容比一般教科書的要...

C 記憶體管理詳解

踏入c 中的雷區 c 記憶體管理詳解 這篇文章回答了我之前的乙個問題,就是分配記憶體用malloc好,還是用new好,今天跑程式的時候,發現malloc老是失敗,氣得不行,最後看到了這篇文章,原來如此,以後堅決用new了。1 有了malloc free為什麼還要new delete?malloc與f...

C記憶體管理詳解

規則1 用malloc或new申請記憶體之後,應該立即檢查指標值是否為null。防止使用指標值為null的記憶體。規則2 不要忘記為陣列和動態記憶體賦初值。防止將未被初始化的記憶體作為右值使用。規則3 避免陣列或指標的下標越界,特別要當心發生 多1 或者 少1 操作。規則4 動態記憶體的申請與釋放必...