天天看點

(轉)從記憶體管 理、記憶體洩漏、記憶體回收探讨C++記憶體管理

http://www.cr173.com/html/18898_all.html

  記憶體管理是c++最令人切齒痛恨的問題,也是c++最有争議的問題,c++高手從中獲得了更好的性能,更大的自由,c++菜鳥的收獲則是一遍一遍的檢查代碼和對

c++的痛恨,但記憶體管理在c++中無處不在,記憶體洩漏幾乎在每個c++程式中都會發生,是以要想成為c++高手,記憶體管理一關是必須要過的,除非放棄

c++,轉到java或者.net,他們的記憶體管理基本是自動的,當然你也放棄了自由和對記憶體的支配權,還放棄了c++超絕的性能。本期專題将從記憶體管

理、記憶體洩漏、記憶體回收這三個方面來探讨c++記憶體管理問題。

1 記憶體管理

偉大的bill gates 曾經失言:

640k ought to be enough for everybody — bill gates 1981

程式員們經常編寫記憶體管理程式,往往提心吊膽。如果不想觸雷,唯一的解決辦法就是發現所有潛伏的地雷并且排除它們,躲是躲不了的。本文的内容比一般教科書的要深入得多,讀者需細心閱讀,做到真正地通曉記憶體管理。

1.1 c++記憶體管理詳解

1.1.1 記憶體配置設定方式

1.1.1.1 配置設定方式簡介

  在c++中,記憶體分成5個區,他們分别是堆、棧、自由存儲區、全局/靜态存儲區和常量存儲區。

  棧,在執行函數時,函數内局部變量的存儲單元都可以在棧上建立,函數執行結束時這些存儲單元自動被釋放。棧記憶體配置設定運算内置于處理器的指令集中,效率很高,但是配置設定的記憶體容量有限。

  堆,就是那些由new配置設定的記憶體塊,他們的釋放編譯器不去管,由我們的應用程式去控制,一般一個new就要對應一個delete。如果程式員沒有釋放掉,那麼在程式結束後,作業系統會自動回收。

  自由存儲區,就是那些由malloc等配置設定的記憶體塊,他和堆是十分相似的,不過它是用free來結束自己的生命的。

  全局/靜态存儲區,全局變量和靜态變量被配置設定到同一塊記憶體中,在以前的c語言中,全局變量又分為初始化的和未初始化的,在c++裡面沒有這個區分了,他們共同占用同一塊記憶體區。

  常量存儲區,這是一塊比較特殊的存儲區,他們裡面存放的是常量,不允許修改。

1.1.1.2 明确區分堆與棧

  在bbs上,堆與棧的區分問題,似乎是一個永恒的話題,由此可見,初學者對此往往是混淆不清的,是以我決定拿他第一個開刀。

  首先,我們舉一個例子:

void f() { int* p=new int[5]; }

  這條短短的一句話就包含了堆與棧,看到new,我們首先就應該想到,我們配置設定了一塊堆記憶體,那麼指針p呢?他配置設定的是一塊棧記憶體,是以這句話的意思就是:在棧記憶體中存放了一個指向一塊堆記憶體的指針p。在程式會先确定在堆中配置設定記憶體的大小,然後調用operator

new配置設定記憶體,然後傳回這塊記憶體的首位址,放入棧中,他在vc6下的彙編代碼如下:

00401028 push 14h

0040102a call operator

new (00401060)

0040102f add esp,4

00401032 mov dword ptr

[ebp-8],eax

00401035 mov eax,dword ptr [ebp-8]

00401038 mov dword

ptr [ebp-4],eax

  這裡,我們為了簡單并沒有釋放記憶體,那麼該怎麼去釋放呢?是delete p麼?澳,錯了,應該是delete

[]p,這是為了告訴編譯器:我删除的是一個數組,vc6就會根據相應的cookie資訊去進行釋放記憶體的工作。

1.1.1.3 堆和棧究竟有什麼差別?

  好了,我們回到我們的主題:堆和棧究竟有什麼差別?

  主要的差別由以下幾點:

  1、管理方式不同;

  2、空間大小不同;

  3、能否産生碎片不同;

  4、生長方向不同;

  5、配置設定方式不同;

  6、配置設定效率不同;

  管理方式:對于棧來講,是由編譯器自動管理,無需我們手工控制;對于堆來說,釋放工作由程式員控制,容易産生memory leak。

  空間大小:一般來講在32位系統下,堆記憶體可以達到4g的空間,從這個角度來看堆記憶體幾乎是沒有什麼限制的。但是對于棧來講,一般都是有一定的空間大小的,例如,在vc6下面,預設的棧空間大小是1m(好像是,記不清楚了)。當然,我們可以修改:

  打開工程,依次操作菜單如下:project->setting->link,在category

中選中output,然後在reserve中設定堆棧的最大值和commit。

  注意:reserve最小值為4byte;commit是保留在虛拟記憶體的頁檔案裡面,它設定的較大會使棧開辟較大的值,可能增加記憶體的開銷和啟動時間。

  碎片問題:對于堆來講,頻繁的new/delete勢

必會造成記憶體空間的不連續,進而造成大量的碎片,使程式效率降低。對于棧來講,則不會存在這個問題,因為棧是先進後出的隊列,他們是如此的一一對應,以至

于永遠都不可能有一個記憶體塊從棧中間彈出,在他彈出之前,在他上面的後進的棧内容已經被彈出,詳細的可以參考資料結構,這裡我們就不再一一讨論了。

  生長方向:對于堆來講,生長方向是向上的,也就是向着記憶體位址增加的方向;對于棧來講,它的生長方向是向下的,是向着記憶體位址減小的方向增長。

  配置設定方式:堆都是動态配置設定的,沒有靜态配置設定的堆。棧有2種配置設定方式:靜态配置設定和動态配置設定。靜态配置設定是編譯器完成的,比如局部變量的配置設定。動态配置設定由alloca函數進行配置設定,但是棧的動态配置設定和堆是不同的,他的動态配置設定是由編譯器進行釋放,無需我們手工實作。

  配置設定效率:棧是機器系統提供的資料結構,計算機會在底層對棧提供支援:配置設定專門的寄存器存放棧的位址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是c/c++函數庫提供的,它的機制是很複雜的,例如為了配置設定一塊記憶體,庫函數會按照一定的算法(具體的算法可以參考資料結構/作業系統)在堆記憶體中搜尋可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由于記憶體碎片太多),就有可能調用系統功能去增加程式資料段的記憶體空間,這樣就有機會分到足夠大小的記憶體,然後進行傳回。顯然,堆的效率比棧要低得多。

  從這裡我們可以看到,堆和棧相比,由于大量new/delete的使用,容易造成大量的記憶體碎片;由于沒有專門的系統支援,效率很低;由于可能引發使用者态和核心态的切換,記憶體的申請,代價變得更加昂貴。是以棧在程式中是應用最廣泛的,就算是函數的調用也利用棧去完成,函數調用過程中的參數,傳回位址,ebp和局部變量都采用棧的方式存放。是以,我們推薦大家盡量用棧,而不是用堆。

  雖然棧有如此衆多的好處,但是由于和堆相比不是那麼靈活,有時候配置設定大量的記憶體空間,還是用堆好一些。

無論是堆還是棧,都要防止越界現象的發生(除非你是故意使其越界),因為越界的結果要麼是程式崩潰,要麼是摧毀程式的堆、棧結構,産生以想不到的結果,就算是在你的程式運作過程中,沒有發生上面的問題,你還是要小心,說不定什麼時候就崩掉,那時候debug可是相當困難的:)

1.1.2 控制c++的記憶體配置設定

  在嵌入式系統中使用c++的一個常見問題是記憶體配置設定,即對new 和 delete 操作符的失控。

  具有諷刺意味的是,問題的根源卻是c++對記憶體的管理非常的容易而且安全。具體地說,當一個對象被消除時,它的析構函數能夠安全的釋放所配置設定的記憶體。

  這當然是個好事情,但是這種使用的簡單性使得程式員們過度使用new 和

delete,而不注意在嵌入式c++環境中的因果關系。并且,在嵌入式系統中,由于記憶體的限制,頻繁的動态配置設定不定大小的記憶體會引起很大的問題以及堆破碎的風險。

  作為忠告,保守的使用記憶體配置設定是嵌入式環境中的第一原則。

  但當你必須要使用new 和delete時,你不得不控制c++中的記憶體配置設定。你需要用一個全局的new

和delete來代替系統的記憶體配置設定符,并且一個類一個類的重載new 和delete。

  一個防止堆破碎的通用方法是從不同固定大小的記憶體持中配置設定不同類型的對象。對每個類重載new 和delete就提供了這樣的控制。

1.1.2.1 重載全局的new和delete操作符

  可以很容易地重載new 和 delete 操作符,如下所示:

void * operator new(size_t size)

{

void

*p = malloc(size);

return (p);

}

void operator delete(void

*p);

free(p);

  這段代碼可以代替預設的操作符來滿足記憶體配置設定的請求。出于解釋c++的目的,我們也可以直接調用malloc() 和free()。

  也可以對單個類的new 和 delete 操作符重載。這是你能靈活的控制對象的記憶體配置設定。

class testclass {

public:

void *

operator new(size_t size);

void operator delete(void *p);

// ..

other members here ...

};

void *testclass::operator new(size_t

size)

void *p = malloc(size); // replace this with alternative

allocator

void testclass::operator delete(void

*p)

free(p); // replace this with alternative

de-allocator

  所有testclass 對象的記憶體配置設定都采用這段代碼。更進一步,任何從testclass 繼承的類也都采用這一方式,除非它自己也重載了new 和

delete 操作符。通過重載new 和 delete 操作符的方法,你可以自由地采用不同的配置設定政策,從不同的記憶體池中配置設定不同的類對象。

1.1.2.2 為單個的類重載 new[ ]和delete[ ]

  必須小心對象數組的配置設定。你可能希望調用到被你重載過的new 和 delete 操作符,但并不如此。記憶體的請求被定向到全局的new[ ]和delete[

] 操作符,而這些記憶體來自于系統堆。

  c++将對象數組的記憶體配置設定作為一個單獨的操作,而不同于單個對象的記憶體配置設定。為了改變這種方式,你同樣需要重載new[ ] 和 delete[

]操作符。

operator new[ ](size_t size);

void operator delete[ ](void *p);

//

.. other members here ..

void *testclass::operator new[ ](size_t

void *p = malloc(size);

testclass::operator delete[ ](void *p)

int

main(void)

testclass *p = new testclass[10];

// ... etc

...

delete[ ] p;

但是注意:對于多數c++的實作,new[]操作符中的個數參數是數組的大小加上額外的存儲對象數目的一些位元組。在你的記憶體配置設定機制重要考慮的這一點。你應該盡量避免配置設定對象數組,進而使你的記憶體配置設定政策簡單。

1.1.3 常見的記憶體錯誤及其對策

發生記憶體錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程式運作時才能捕捉到。而這些錯誤大多沒有明顯的症狀,時隐時現,增加了改錯的難度。有時使用者怒氣沖沖地把你找來,程式卻沒有發生任何問題,你一走,錯誤又發作了。

常見的記憶體錯誤及其對策如下:

  * 記憶體配置設定未成功,卻使用了它。

  程式設計新手常犯這種錯誤,因為他們沒有意識到記憶體配置設定會不成功。常用解決辦法是,在使用記憶體之前檢查指針是否為null。如果指針p是函數的參數,那麼在函數的入口處用assert(p!=null)進行

  檢查。如果是用malloc或new來申請記憶體,應該用if(p==null) 或if(p!=null)進行防錯處理。

  * 記憶體配置設定雖然成功,但是尚未初始化就引用它。

  犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為記憶體的預設初值全為零,導緻引用初值錯誤(例如數組)。

記憶體的預設初值究竟是什麼并沒有統一的标準,盡管有些時候為零值,我們甯可信其無不可信其有。是以無論用何種方式建立數組,都别忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。

  * 記憶體配置設定成功并且已經初始化,但操作越過了記憶體的邊界。

  例如在使用數組時經常發生下标“多1”或者“少1”的操作。特别是在for循環語句中,循環次數很容易搞錯,導緻數組操作越界。

  * 忘記了釋放記憶體,造成記憶體洩露。

  含有這種錯誤的函數每被調用一次就丢失一塊記憶體。剛開始時系統的記憶體充足,你看不到錯誤。終有一次程式突然死掉,系統出現提示:記憶體耗盡。

  動态記憶體的申請與釋放必須配對,程式中malloc與free的使用次數一定要相同,否則肯定有錯誤(new/delete同理)。

  * 釋放了記憶體卻繼續使用它。

  有三種情況:

  (1)程式中的對象調用關系過于複雜,實在難以搞清楚某個對象究竟是否已經釋放了記憶體,此時應該重新設計資料結構,從根本上解決對象管理的混亂局面。

  (2)函數的return語句寫錯了,注意不要傳回指向“棧記憶體”的“指針”或者“引用”,因為該記憶體在函數體結束時被自動銷毀。

  (3)使用free或delete釋放了記憶體後,沒有将指針設定為null。導緻産生“野指針”。

  【規則1】用malloc或new申請記憶體之後,應該立即檢查指針值是否為null。防止使用指針值為null的記憶體。

  【規則2】不要忘記為數組和動态記憶體賦初值。防止将未被初始化的記憶體作為右值使用。

  【規則3】避免數組或指針的下标越界,特别要當心發生“多1”或者“少1”操作。

  【規則4】動态記憶體的申請與釋放必須配對,防止記憶體洩漏。

  【規則5】用free或delete釋放了記憶體之後,立即将指針設定為null,防止産生“野指針”。

1.1.4 指針與數組的對比

  c++/c程式中,指針和數組在不少地方可以互相替換着用,讓人産生一種錯覺,以為兩者是等價的。

  數組要麼在靜态存儲區被建立(如全局數組),要麼在棧上被建立。數組名對應着(而不是指向)一塊記憶體,其位址與容量在生命期内保持不變,隻有數組的内容可以改變。

  指針可以随時指向任意類型的記憶體塊,它的特征是“可變”,是以我們常用指針來操作動态記憶體。指針遠比數組靈活,但也更危險。

  下面以字元串為例比較指針與數組的特性。

1.1.4.1 修改内容

下面示例中,字元數組a的容量是6個字元,其内容為hello。a的内容可以改變,如a[0]=

‘x’。指針p指向常量字元串“world”(位于靜态存儲區,内容為world),常量字元串的内容是不可以被修改的。從文法上看,編譯器并不覺得語句p[0]=

‘x’有什麼不妥,但是該語句企圖修改常量字元串的内容而導緻運作錯誤。

char a[] = “hello”;

a[0] = ‘x’;

cout

<< a << endl;

char *p = “world”; // 注意p指向常量字元串

p[0] =

‘x’; // 編譯器不能發現該錯誤

cout << p << endl;

1.1.4.2 内容複制與比較

  不能對數組名進行直接複制與比較。若想把數組a的内容複制給數組b,不能用語句 b = a

,否則将産生編譯錯誤。應該用标準庫函數strcpy進行複制。同理,比較b和a的内容是否相同,不能用if(b==a)

來判斷,應該用标準庫函數strcmp進行比較。

語句p = a

并不能把a的内容複制指針p,而是把a的位址賦給了p。要想複制a的内容,可以先用庫函數malloc為p申請一塊容量為strlen(a)+1個字元的記憶體,再用strcpy進行字元串複制。同理,語句if(p==a)

比較的不是内容而是位址,應該用庫函數strcmp來比較。

// 數組…

char a[] = "hello";

char

b[10];

strcpy(b, a); // 不能用 b = a;

if(strcmp(b, a) == 0) // 不能用 if

(b == a)

// 指針…

int len = strlen(a);

char *p = (char

*)malloc(sizeof(char)*(len+1));

strcpy(p,a); // 不要用 p =

a;

if(strcmp(p, a) == 0) // 不要用 if (p == a)

1.1.4.3 計算記憶體容量

用運算符sizeof可以計算出數組的容量(位元組數)。如下示例中,sizeof(a)的值是12(注意别忘了’’)。指針p指向a,但是sizeof(p)的值卻是4。這是因為sizeof(p)得到的是一個指針變量的位元組數,相當于sizeof(char*),而不是p所指的記憶體容量。c++/c語言沒有辦法知道指針所指的記憶體容量,除非在申請記憶體時記住它。

char a[] = "hello world";

char *p =

cout<< sizeof(a) << endl; // 12位元組

cout<<

sizeof(p) << endl; // 4位元組

  

注意當數組作為函數的參數進行傳遞時,該數組自動退化為同類型的指針。如下示例中,不論數組a的容量是多少,sizeof(a)始終等于sizeof(char

*)。

void func(char a[100])

 cout<<

sizeof(a) << endl; // 4位元組而不是100位元組

1.1.5 指針參數是如何傳遞記憶體的?

如果函數的參數是一個指針,不要指望用該指針去申請動态記憶體。如下示例中,test函數的語句getmemory(str,

200)并沒有使str獲得期望的記憶體,str依舊是null,為什麼?

void getmemory(char *p, int num)

 p =

(char *)malloc(sizeof(char) * num);

void test(void)

 char

*str = null;

 getmemory(str, 100); // str 仍然為 null

 strcpy(str,

"hello"); // 運作錯誤

毛病出在函數getmemory中。編譯器總是要為函數的每個參數制作臨時副本,指針參數p的副本是 _p,編譯器使 _p =

p。如果函數體内的程式修改了_p的内容,就導緻參數p的内容作相應的修改。這就是指針可以用作輸出參數的原因。在本例中,_p申請了新的記憶體,隻是把_p所指的記憶體位址改變了,但是p絲毫未變。是以函數getmemory并不能輸出任何東西。事實上,每執行一次getmemory就會洩露一塊記憶體,因為沒有用free釋放記憶體。

如果非得要用指針參數去申請記憶體,那麼應該改用“指向指針的指針”,見示例:

void getmemory2(char **p, int

num)

 *p = (char *)malloc(sizeof(char) * num);

test2(void)

 char *str = null;

 getmemory2(&str, 100); //

注意參數是 &str,而不是str

 strcpy(str, "hello");

 cout<< str

<< endl;

 free(str);

由于“指向指針的指針”這個概念不容易了解,我們可以用函數傳回值來傳遞動态記憶體。這種方法更加簡單,見示例:

char *getmemory3(int num)

 char *p =

 return p;

test3(void)

 str =

getmemory3(100);

 cout<< str <<

endl;

用函數傳回值來傳遞動态記憶體這種方法雖然好用,但是常常有人把return語句用錯了。這裡強調不要用return語句傳回指向“棧記憶體”的指針,因為該記憶體在函數結束時自動消亡,見示例:

char *getstring(void)

 char p[] =

"hello world";

 return p; // 編譯器将提出警告

test4(void)

 str = getstring(); // str

的内容是垃圾

 cout<< str << endl;

用調試器逐漸跟蹤test4,發現執行str = getstring語句後str不再是null指針,但是str的内容不是“hello

world”而是垃圾。

如果把上述示例改寫成如下示例,會怎麼樣?

char *getstring2(void)

void test5(void)

 char *str =

null;

 str = getstring2();

函數test5運作雖然不會出錯,但是函數getstring2的設計概念卻是錯誤的。因為getstring2内的“hello

world”是常量字元串,位于靜态存儲區,它在程式生命期内恒定不變。無論什麼時候調用getstring2,它傳回的始終是同一個“隻讀”的記憶體塊。

1.1.6 杜絕“野指針”

  “野指針”不是null指針,是指向“垃圾”記憶體的指針。人們一般不會錯用null指針,因為用if語句很容易判斷。但是“野指針”是很危險的,if語句對它不起作用。

“野指針”的成因主要有兩種:

(1)指針變量沒有被初始化。任何指針變量剛被建立時不會自動成為null指針,它的預設值是随機的,它會亂指一氣。是以,指針變量在建立的同時應當被初始化,要麼将指針設定為null,要麼讓它指向合法的記憶體。例如

char *p = null;

char *str = (char *)

malloc(100);

(2)指針p被free或者delete之後,沒有置為null,讓人誤以為p是個合法的指針。

(3)指針操作超越了變量的作用域範圍。這種情況讓人防不勝防,示例程式如下:

class a

 public:

  void

func(void){ cout << “func of class a” << endl; }

test(void)

 a *p;

 {

  a a;

  p = &a; // 注意 a

的生命期

 }

 p->func(); // p是“野指針”

函數test在執行語句p->func()時,對象a已經消失,而p是指向a的,是以p就成了“野指針”。但奇怪的是我運作這個程式時居然沒有出錯,這可能與編譯器有關。

1.1.7 有了malloc/free為什麼還要new/delete?

  malloc與free是c++/c語言的标準庫函數,new/delete是c++的運算符。它們都可用于申請動态記憶體和釋放記憶體。

  對于非内部資料類型的對象而言,光用maloc/free無法滿足動态對象的要求。對象在建立的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。由于malloc/free是庫函數而不是運算符,不在編譯器控制權限之内,不能夠把執行構造函數和析構函數的任務強加于malloc/free。

是以c++語言需要一個能完成動态記憶體配置設定和初始化工作的運算符new,以及一個能完成清理與釋放記憶體工作的運算符delete。注意new/delete不是庫函數。我們先看一看malloc/free和new/delete如何實作對象的動态記憶體管理,見示例:

class obj

 public :

  obj(void){

cout << “initialization” << endl; }

  ~obj(void){ cout

<< “destroy” << endl; }

  void initialize(void){ cout

<< “initialization” << endl; }

  void destroy(void){ cout

usemallocfree(void)

 obj *a = (obj *)malloc(sizeof(obj)); //

申請動态記憶體

 a->initialize(); // 初始化

 //…

 a->destroy(); //

清除工作

 free(a); // 釋放記憶體

void usenewdelete(void)

 obj *a

= new obj; // 申請動态記憶體并且初始化

 delete a; //

清除并且釋放記憶體

  類obj的函數initialize模拟了構造函數的功能,函數destroy模拟了析構函數的功能。函數usemallocfree中,由于malloc/free不能執行構造函數與析構函數,必須調用成員函數initialize和destroy來完成初始化與清除工作。函數usenewdelete則簡單得多。

  是以我們不要企圖用malloc/free來完成動态對象的記憶體管理,應該用new/delete。由于内部資料類型的“對象”沒有構造與析構的過程,對它們而言malloc/free和new/delete是等價的。

  既然new/delete的功能完全覆寫了malloc/free,為什麼c++不把malloc/free淘汰出局呢?這是因為c++程式經常要調用c函數,而c程式隻能用malloc/free管理動态記憶體。

如果用free釋放“new建立的動态對象”,那麼該對象因無法執行析構函數而可能導緻程式出錯。如果用delete釋放“malloc申請的動态記憶體”,結果也會導緻程式出錯,但是該程式的可讀性很差。是以new/delete必須配對使用,malloc/free也一樣。

1.1.8 記憶體耗盡怎麼辦?

  如果在申請動态記憶體時找不到足夠大的記憶體塊,malloc和new将傳回null指針,宣告記憶體申請失敗。通常有三種方式處理“記憶體耗盡”問題。

  (1)判斷指針是否為null,如果是則馬上用return語句終止本函數。例如:

void func(void)

 a *a = new

 if(a ==

null)

  return;

 …

 (2)判斷指針是否為null,如果是則馬上用exit(1)終止整個程式的運作。例如:

 if(a == null)

  cout << “memory exhausted” <<

  exit(1);

  (3)為new和malloc設定異常處理函數。例如visual

c++可以用_set_new_hander函數為new設定使用者自己定義的異常處理函數,也可以讓malloc享用與new相同的異常處理函數。詳細内容請參考c++使用手冊。

  上述(1)(2)方式使用最普遍。如果一個函數内有多處需要申請動态記憶體,那麼方式(1)就顯得力不從心(釋放記憶體很麻煩),應該用方式(2)來處理。

  很多人不忍心用exit(1),問:“不編寫出錯處理程式,讓作業系統自己解決行不行?”

  不行。如果發生“記憶體耗盡”這樣的事情,一般說來應用程式已經無藥可救。如果不用exit(1)

把壞程式殺死,它可能會害死作業系統。道理如同:如果不把歹徒擊斃,歹徒在老死之前會犯下更多的罪。

  有一個很重要的現象要告訴大家。對于32位以上的應用程式而言,無論怎樣使用malloc與new,幾乎不可能導緻“記憶體耗盡”。我在windows

98下用visual

c++編寫了測試程式,見示例7。這個程式會無休止地運作下去,根本不會終止。因為32位作業系統支援“虛存”,記憶體用完了,自動用硬碟空間頂替。我隻聽到硬碟嘎吱嘎吱地響,window

98已經累得對鍵盤、滑鼠毫無反應。

  我可以得出這麼一個結論:對于32位以上的應用程式,“記憶體耗盡”錯誤處理程式毫無用處。這下可把unix和windows程式員們樂壞了:反正錯誤處理程式不起作用,我就不寫了,省了很多麻煩。

我不想誤導讀者,必須強調:不加錯誤處理将導緻程式的品質很差,千萬不可因小失大。

void main(void)

 float *p =

 while(true)

  p = new float[1000000];

  cout

<< “eat memory” <<

  if(p==null)

   exit(1);

1.1.9 malloc/free的使用要點

函數malloc的原型如下:

void * malloc(size_t

size);

用malloc申請一塊長度為length的整數類型的記憶體,程式如下:

int *p = (int *) malloc(sizeof(int) *

length);

我們應當把注意力集中在兩個要素上:“類型轉換”和“sizeof”。

* malloc傳回值的類型是void *,是以在調用malloc時要顯式地進行類型轉換,将void * 轉換成所需要的指針類型。

* malloc函數本身并不識别要申請的記憶體是什麼類型,它隻關心記憶體的總位元組數。我們通常記不住int,

float等資料類型的變量的确切位元組數。例如int變量在16位系統下是2個位元組,在32位下是4個位元組;而float變量在16位系統下是4個位元組,在32位下也是4個位元組。最好用以下程式作一次測試:

cout << sizeof(char) <<

cout << sizeof(int) << endl;

cout <<

sizeof(unsigned int) << endl;

cout << sizeof(long) <<

cout << sizeof(unsigned long) << endl;

<< sizeof(float) << endl;

cout << sizeof(double)

cout << sizeof(void *) <<

在malloc的“()”中使用sizeof運算符是良好的風格,但要當心有時我們會昏了頭,寫出 p =

malloc(sizeof(p))這樣的程式來。

函數free的原型如下:

void free( void * memblock

);

為什麼free函數不象malloc函數那樣複雜呢?這是因為指針p的類型以及它所指的記憶體的容量事先都是知道的,語句free(p)能正确地釋放記憶體。如果p是null指針,那麼free對p無論操作多少次都不會出問題。如果p不是null指針,那麼free對p連續操作兩次就會導緻程式運作錯誤。

1.1.10 new/delete的使用要點

運算符new使用起來要比函數malloc簡單得多,例如:

int *p1 = (int *)malloc(sizeof(int) *

int *p2 = new int[length];

這是因為new内置了sizeof、類型轉換和類型安全檢查功能。對于非内部資料類型的對象而言,new在建立動态對象的同時完成了初始化工作。如果對象有多個構造函數,那麼new的語句也可以有多種形式。例如

  obj(void);

// 無參數的構造函數

  obj(int x); // 帶一個參數的構造函數

  …

 obj *a = new obj;

 obj *b = new obj(1); //

初值為1

 delete a;

 delete b;

如果用new建立對象數組,那麼隻能使用對象的無參數構造函數。例如:

obj *objects = new obj[100]; //

建立100個動态對象

不能寫成:

obj *objects = new obj[100](1);//

建立100個動态對象的同時賦初值1

在用delete釋放對象數組時,留意不要丢了符号‘[]’。例如:

delete []objects; // 正确的用法

delete objects;

// 錯誤的用法

後者有可能引起程式崩潰和記憶體洩漏。

1.2 c++中的健壯指針和資源管理

  我最喜歡的對資源的定義是:"任何在你的程式中獲得并在此後釋放的東西?quot;記憶體是一個相當明顯的資源的例子。它需要用new來獲得,用delete來釋放。同時也有許多其它類型的資源檔案句柄、重要的片斷、windows中的gdi資源,等等。将資源的概念推廣到程式中建立、釋放的所有對象也是十分友善的,無論對象是在堆中配置設定的還是在棧中或者是在全局作用于内生命的。

  對于給定的資源的擁有着,是負責釋放資源的一個對象或者是一段代碼。所有權分立為兩種級别——自動的和顯式的(automatic and

explicit),如果一個對象的釋放是由語言本身的機制來保證的,這個對象的就是被自動地所有。例如,一個嵌入在其他對象中的對象,他的清除需要其他對象來在清除的時候保證。外面的對象被看作嵌入類的所有者。

  類似地,每個在棧上建立的對象(作為自動變量)的釋放(破壞)是在控制流離開了對象被定義的作用域的時候保證的。這種情況下,作用于被看作是對象的所有者。注意所有的自動所有權都是和語言的其他機制相容的,包括異常。無論是如何退出作用域的——正常流程控制退出、一個break語句、一個return、一個goto、或者是一個throw——自動資源都可以被清除。

  到目前為止,一切都很好!問題是在引入指針、句柄和抽象的時候産生的。如果通過一個指針通路一個對象的話,比如對象在堆中配置設定,c++不自動地關注它的釋放。程式員必須明确的用适當的程式方法來釋放這些資源。比如說,如果一個對象是通過調用new來建立的,它需要用delete來回收。一個檔案是用createfile(win32

api)打開的,它需要用closehandle來關閉。用entercritialsection進入的臨界區(critical

section)需要leavecriticalsection退出,等等。一個"裸"指針,檔案句柄,或者臨界區狀态沒有所有者來確定它們的最終釋放。基本的資源管理的前提就是確定每個資源都有他們的所有者。

1.2.1 第一條規則(raii)

  一個指針,一個句柄,一個臨界區狀态隻有在我們将它們封裝入對象的時候才會擁有所有者。這就是我們的第一規則:在構造函數中配置設定資源,在析構函數中釋放資源。

  當你按照規則将所有資源封裝的時候,你可以保證你的程式中沒有任何的資源洩露。這點在當封裝對象(encapsulating object)

在棧中建立或者嵌入在其他的對象中的時候非常明顯。但是對那些動态申請的對象呢?不要急!任何動态申請的東西都被看作一種資源,并且要按照上面提到的方法

進行封裝。這一對象封裝對象的鍊不得不在某個地方終止。它最終終止在最進階的所有者,自動的或者是靜态的。這些分别是對離開作用域或者程式時釋放資源的保 證。

  下面是資源封裝的一個經典例子。在一個多線程的應用程式中,線程之間共享對象的問題是通過用這樣一個對象聯系臨界區來解決的。每一個需要通路共享資源的客戶需要獲得臨界區。例如,這可能是win32下臨界區的實作方法。

class critsect

 friend class

lock;

  critsect () { initializecriticalsection

(&_critsection); }

  ~critsect () { deletecriticalsection

 private:

  void acquire

()

  {

   entercriticalsection (&_critsection);

  }

release ()

   leavecriticalsection

(&_critsection);

  critical_section

_critsection;

  這裡聰明的部分是我們確定每一個進入臨界區的客戶最後都可以離開。"進入"臨界區的狀态是一種資源,并應當被封裝。封裝器通常被稱作一個鎖(lock)。

class lock

  lock

(critsect& critsect) : _critsect

(critsect)

   _critsect.acquire ();

  ~lock

   _critsect.release ();

 private

  critsect

& _critsect;

  鎖一般的用法如下:

void shared::act () throw (char

*)

 lock lock (_critsect);

 // perform action —— may

throw

 // automatic destructor of lock

  注意無論發生什麼,臨界區都會借助于語言的機制保證釋放。

  還有一件需要記住的事情——每一種資源都需要被分别封裝。這是因為資源配置設定是一

個非常容易出錯的操作,是要資源是有限提供的。我們會假設一個失敗的資源配置設定會導緻一個異常——事實上,這會經常的發生。是以如果你想試圖用一個石頭打兩

隻鳥的話,或者在一個構造函數中申請兩種形式的資源,你可能就會陷入麻煩。隻要想想在一種資源配置設定成功但另一種失敗抛出異常時會發生什麼。因為構造函數還

沒有全部完成,析構函數不可能被調用,第一種資源就會發生洩露。

這種情況可以非常簡單的避免。無論何時你有一個需要兩種以上資源的類時,寫兩個小的封裝器将它們嵌入你的類中。每一個嵌入的構造都可以保證删除,即使包裝類沒有構造完成。

1.2.2 smart pointers

  我們至今還沒有讨論最常見類型的資源——用操作符new配置設定,此後用指針通路的一個對象。我們需要為每個對象分别定義一個封裝類嗎?(事實上,c++标準模闆庫已經有了一個模闆類,叫做auto_ptr,其作用就是提供這種封裝。我們一會兒在回到auto_ptr。)讓我們從一個極其簡單、呆闆但安全的東西開始。看下面的smart

pointer模闆類,它十分堅固,甚至無法實作。

template <class t>

class

smartpointer

  ~smartpointer () { delete _p; }

  t

* operator->() { return _p; }

  t const * operator->() const {

return _p; }

 protected:

  smartpointer (): _p (0) {}

  explicit

smartpointer (t* p): _p (p) {}

  t * _p;

  為什麼要把smartpointer的構造函數設計為protected呢?如果我需要遵守第一條規則,那麼我就必須這樣做。資源——在這裡是class

t的一個對象——必須在封裝器的構造函數中配置設定。但是我不能隻簡單的調用new

t,因為我不知道t的構造函數的參數。因為,在原則上,每一個t都有一個不同的構造函數;我需要為他定義個另外一個封裝器。模闆的用處會很大,為每一個新的類,我可以通過繼承smartpointer定義一個新的封裝器,并且提供一個特定的構造函數。

class smartitem: public

smartpointer<item>

  explicit smartitem (int

i)

  : smartpointer<item> (new item (i))

{}

  為每一個類提供一個smart

pointer真的值得嗎?說實話——不!他很有教學的價值,但是一旦你學會如何遵循第一規則的話,你就可以放松規則并使用一些進階的技術。這一技術是讓smartpointer的構造函數成為public,但是隻是是用它來做資源轉換(resource

transfer)我的意思是用new操作符的結果直接作為smartpointer的構造函數的參數,像這樣:

smartpointer<item> item (new item

(i));

  這個方法明顯更需要自控性,不隻是你,而且包括你的程式小組的每個成員。他們都必須發誓出了作資源轉換外不把構造函數用在人以其他用途。幸運的是,這條規矩很容易得以加強。隻需要在源檔案中查找所有的new即可。

1.2.3 resource transfer

  到目前為止,我們所讨論的一直是生命周期在一個單獨的作用域内的資源。現在我們

要解決一個困難的問題——如何在不同的作用域間安全的傳遞資源。這一問題在當你處理容器的時候會變得十分明顯。你可以動态的建立一串對象,将它們存放至一

個容器中,然後将它們取出,并且在最終安排它們。為了能夠讓這安全的工作——沒有洩露——對象需要改變其所有者。

  這個問題的一個非常顯而易見的解決方法是使用smart

pointer,無論是在加入容器前還是還找到它們以後。這是他如何運作的,你加入release方法到smart pointer中:

t *

smartpointer<t>::release ()

t * ptmp = _p;

_p =

0;

return ptmp;

  注意在release調用以後,smart

pointer就不再是對象的所有者了——它内部的指針指向空。現在,調用了release都必須是一個負責的人并且迅速隐藏傳回的指針到新的所有者對象中。在我們的例子中,容器調用了release,比如這個stack的例子:

void stack::push (smartpointer <item>

& item) throw (char *)

if (_top == maxstack)

throw "stack

overflow";

_arr [_top++] = item.release ();

  同樣的,你也可以再你的代碼中用加強release的可靠性。

相應的pop方法要做些什麼呢?他應該釋放了資源并祈禱調用它的是一個負責的人而且立即作一個資源傳遞它到一個smart

pointer?這聽起來并不好。

1.2.4 strong pointers

  資源管理在内容索引(windows nt server上的一部分,現在是windows

2000)上工作,并且,我對這十分滿意。然後我開始想……這一方法是在這樣一個完整的系統中形成的,如果可以把它内建入語言的本身豈不是一件非常好?我提出了強指針(strong

pointer)和弱指針(weak pointer)。一個strong

pointer會在許多地方和我們這個smartpointer相似--它在超出它的作用域後會清除他所指向的對象。資源傳遞會以強指針指派的形式進行。也可以有weak

pointer存在,它們用來通路對象而不需要所有對象--比如可指派的引用。

  任何指針都必須聲明為strong或者weak,并且語言應該來關注類型轉換的規定。例如,你不可以将weak pointer傳遞到一個需要strong

pointer的地方,但是相反卻可以。push方法可以接受一個strong pointer并且将它轉移到stack中的strong

pointer的序列中。pop方法将會傳回一個strong pointer。把strong pointer的引入語言将會使垃圾回收成為曆史。

  這裡還有一個小問題--修改c++标準幾乎和競選美國總統一樣容易。當我将我的注意告訴給bjarne

stroutrup的時候,他看我的眼神好像是我剛剛要向他借一千美元一樣。

然後我突然想到一個念頭。我可以自己實作strong pointers。畢竟,它們都很想smart

pointers。給它們一個拷貝構造函數并重載指派操作符并不是一個大問題。事實上,這正是标準庫中的auto_ptr有的。重要的是對這些操作給出一個資源轉移的文法,但是這也不是很難。

template <class

t>

smartpointer<t>::smartpointer (smartpointer<t> &

ptr)

_p = ptr.release ();

smartpointer<t>::operator = (smartpointer<t> &

if (_p != ptr._p)

delete _p;

_p = ptr.release

();

  使這整個想法迅速成功的原因之一是我可以以值方式傳遞這種封裝指針!我有了我的蛋糕,并且也可以吃了。看這個stack的新的實作:

class stack

enum { maxstack = 3

stack ()

: _top (0)

void push

(smartpointer<item> & item) throw (char *)

if (_top

>= maxstack)

throw "stack overflow";

_arr [_top++] =

item;

smartpointer<item> pop ()

if (_top ==

0)

return smartpointer<item> ();

return _arr

[--_top];

private

int _top;

smartpointer<item> _arr

[maxstack];

  pop方法強制客戶将其傳回值賦給一個strong

pointer,smartpointer<item>。任何試圖将他對一個普通指針的指派都會産生一個編譯期錯誤,因為類型不比對。此外,因為pop以值方式傳回一個strong

pointer(在pop的聲明時smartpointer<item>後面沒有&符号),編譯器在return時自動進行了一個資源轉換。他調用了operator

=來從數組中提取一個item,拷貝構造函數将他傳遞給調用者。調用者最後擁有了指向pop指派的strong pointer指向的一個item。

我馬上意識到我已經在某些東西之上了。我開始用了新的方法重寫原來的代碼。

1.2.5 parser

我過去有一個老的算術操作分析器,是用老的資源管理的技術寫的。分析器的作用是在分析樹中生成節點,節點是動态配置設定的。例如分析器的expression方法生成一個表達式節點。我沒有時間用strong

pointer去重寫這個分析器。我令expression、term和factor方法以傳值的方式将strong

pointer傳回到node中。看下面的expression方法的實作:

smartpointer<node>

parser::expression()

// parse a term

pnode = term ();

etoken token = _scanner.token();

if ( token ==

tplus || token == tminus )

// expr := term { (‘+‘ | ‘-‘) term

smartpointer<multinode> pmultinode = new sumnode

(pnode);

do

_scanner.accept();

pright = term ();

pmultinode->addchild (pright, (token ==

tplus));

token = _scanner.token();

} while (token == tplus || token

== tminus);

pnode = up_cast<node, multinode>

(pmultinode);

// otherwise expr := term

return pnode; // by

value!

  最開始,term方法被調用。他傳值傳回一個指向node的strong pointer并且立刻把它儲存到我們自己的strong

pointer,pnode中。如果下一個符号不是加号或者減号,我們就簡單的把這個smartpointer以值傳回,這樣就釋放了node的所有權。另外一方面,如果下一個符号是加号或者減号,我們建立一個新的summode并且立刻(直接傳遞)将它儲存到multinode的一個strong

pointer中。這裡,sumnode是從multimode中繼承而來的,而mulitnode是從node繼承而來的。原來的node的所有權轉給了sumnode。

  隻要是他們在被加号和減号分開的時候,我們就不斷的建立terms,我們将這些term轉移到我們的multinode中,同時multinode得到了所有權。最後,我們将指向multinode的strong

pointer向上映射為指向mode的strong pointer,并且将他傳回調用着。

  我們需要對strong

pointers進行顯式的向上映射,即使指針是被隐式的封裝。例如,一個multinode是一個node,但是相同的is-a關系在smartpointer<multinode>和smartpointer<node>之間并不存在,因為它們是分離的類(模闆執行個體)并不存在繼承關系。up-cast模闆是像下面這樣定義的:

template<class to, class

from>

inline smartpointer<to> up_cast

(smartpointer<from> & from)

return

smartpointer<to> (from.release ());

  如果你的編譯器支援新加入标準的成員模闆(member

template)的話,你可以為smartpointer<t>定義一個新的構造函數用來從接受一個class u。

template

<class u> smartpointer<t>::smartpointer (sprt<u> &

uptr)

: _p (uptr.release ())

  這裡的這個花招是模闆在u不是t的子類的時候就不會編譯成功(換句話說,隻在u is-a

t的時候才會編譯)。這是因為uptr的緣故。release()方法傳回一個指向u的指針,并被指派為_p,一個指向t的指針。是以如果u不是一個t的話,指派會導緻一個編譯時刻錯誤。

std::auto_ptr

後來我意識到在stl中的auto_ptr模闆,就是我的strong

pointer。在那時候還有許多的實作差異(auto_ptr的release方法并不将内部的指針清零--你的編譯器的庫很可能用的就是這種陳舊的實作),但是最後在标準被廣泛接受之前都被解決了。

1.2.6 transfer semantics

  目前為止,我們一直在讨論在c++程式中資源管理的方法。宗旨是将資源封裝到一些輕量級的類中,并由類負責它們的釋放。特别的是,所有用new操作符配置設定的資源都會被儲存并傳遞進strong

pointer(标準庫中的auto_ptr)的内部。

  這裡的關鍵詞是傳遞(passing)。一個容器可以通過傳值傳回一個strong

pointer來安全的釋放資源。容器的客戶隻能夠通過提供一個相應的strong

pointer來儲存這個資源。任何一個将結果賦給一個"裸"指針的做法都立即會被編譯器發現。

auto_ptr<item> item = stack.pop (); //

ok

item * p = stack.pop (); // error! type

mismatch.

  以傳值方式被傳遞的對象有value semantics 或者稱為 copy semantics。strong

pointers是以值方式傳遞的--但是我們能說它們有copy

semantics嗎?不是這樣的!它們所指向的對象肯定沒有被拷貝過。事實上,傳遞過後,源auto_ptr不在通路原有的對象,并且目标auto_ptr成為了對象的唯一擁有者(但是往往auto_ptr的舊的實作即使在釋放後仍然保持着對對象的所有權)。自然而然的我們可以将這種新的行為稱作transfer

semantics。

  拷貝構造函數(copy construcor)和指派操作符定義了auto_ptr的transfer

semantics,它們用了非const的auto_ptr引用作為它們的參數。

auto_ptr (auto_ptr<t> &

ptr);

auto_ptr & operator = (auto_ptr<t> &

  這是因為它們确實改變了他們的源--剝奪了對資源的所有權。

通過定義相應的拷貝構造函數和重載指派操作符,你可以将transfer

semantics加入到許多對象中。例如,許多windows中的資源,比如動态建立的菜單或者位圖,可以用有transfer

semantics的類來封裝。

1.2.7 strong vectors

  标準庫隻在auto_ptr中支援資源管理。甚至連最簡單的容器也不支援ownership

semantics。你可能想将auto_ptr和标準容器組合到一起可能會管用,但是并不是這樣的。例如,你可能會這樣做,但是會發現你不能夠用标準的方法來進行索引。

vector< auto_ptr<item> >

autovector;

  這種建造不會編譯成功;

item * item = autovector

[0];

  另一方面,這會導緻一個從autovect到auto_ptr的所有權轉換:

auto_ptr<item> item = autovector

  我們沒有選擇,隻能夠構造我們自己的strong vector。最小的接口應該如下:

auto_vector

explicit auto_vector (size_t capacity =

0);

t const * operator [] (size_t i) const;

t * operator [] (size_t

i);

void assign (size_t i, auto_ptr<t> & p);

assign_direct (size_t i, t * p);

void push_back (auto_ptr<t>

& p);

auto_ptr<t> pop_back ();

  你也許會發現一個非常防禦性的設計态度。我決定不提供一個對vector的左值索引的通路,取而代之,如果你想設定(set)一個值的話,你必須用assign或者assign_direct方法。我的觀點是,資源管理不應該被忽視,同時,也不應該在所有的地方濫用。在我的經驗裡,一個strong

vector經常被許多push_back方法充斥着。

  strong vector最好用一個動态的strong pointers的數組來實作:

void grow (size_t

reqcapacity);

auto_ptr<t> *_arr;

size_t _capacity;

size_t

_end;

  grow方法申請了一個很大的auto_ptr<t>的數組,将所有的東西從老的書組類轉移出來,在其中交換,并且删除原來的數組。

  auto_vector的其他實作都是十分直接的,因為所有資源管理的複雜度都在auto_ptr中。例如,assign方法簡單的利用了重載的指派操作符來删除原有的對象并轉移資源到新的對象:

void assign (size_t i, auto_ptr<t>

& p)

_arr [i] = p;

  我已經讨論了push_back和pop_back方法。push_back方法傳值傳回一個auto_ptr,因為它将所有權從auto_vector轉換到auto_ptr中。

  對auto_vector的索引通路是借助auto_ptr的get方法來實作的,get簡單的傳回一個内部指針。

t * operator [] (size_t i)

_arr [i].get ();

  沒有容器可以沒有iterator。我們需要一個iterator讓auto_vector看起來更像一個普通的指針向量。特别是,當我們廢棄iterator的時候,我們需要的是一個指針而不是auto_ptr。我們不希望一個auto_vector的iterator在無意中進行資源轉換。

template<class t>

auto_iterator: public

iterator<random_access_iterator_tag, t

*>

auto_iterator () : _pp (0) {}

auto_iterator

(auto_ptr<t> * pp) : _pp (pp) {}

bool operator !=

(auto_iterator<t> const & it) const

{ return it._pp != _pp;

auto_iterator const & operator++ (int) { return _pp++;

auto_iterator operator++ () { return ++_pp; }

t * operator * () {

return _pp->get (); }

auto_ptr<t> *

_pp;

我們給auto_vect提供了标準的begin和end方法來找回iterator:

class auto_vector

typedef

auto_iterator<t> iterator;

iterator begin () { return _arr;

iterator end () { return _arr + _end;

}; 

  你也許會問我們是否要利用資源管理重新實作每一個标準的容器?幸運的是,不;事實是strong

vector解決了大部分所有權的需求。當你把你的對象都安全的放置到一個strong

vector中,你可以用所有其它的容器來重新安排(weak)pointer。

設想,例如,你需要對一些動态配置設定的對象排序的時候。你将它們的指針儲存到一個strong vector中。然後你用一個标準的vector來儲存從strong

vector中獲得的weak指針。你可以用标準的算法對這個vector進行排序。這種中介vector叫做permutation

vector。相似的,你也可以用标準的maps, priority queues, heaps, hash tables等等。

1.2.8 code inspection

  如果你嚴格遵照資源管理的條款,你就不會再資源洩露或者兩次删除的地方遇到麻煩。你也降低了通路野指針的幾率。同樣的,遵循原有的規則,用delete删除用new申請的德指針,不要兩次删除一個指針。你也不會遇到麻煩。但是,那個是更好的注意呢?

  這兩個方法有一個很大的不同點。就是和尋找傳統方法的bug相比,找到違反資源管理的規定要容易的多。後者僅需要一個代碼檢測或者一個運作測試,而前者則在代碼中隐藏得很深,并需要很深的檢查。

  設想你要做一段傳統的代碼的記憶體洩露檢查。第一件事,你要做的就是grep所有在代碼中出現的new,你需要找出被配置設定空間地指針都作了什麼。你需要确定導緻删除這個指針的所有的執行路徑。你需要檢查break語句,過程傳回,異常。原有的指針可能賦給另一個指針,你對這個指針也要做相同的事。

  相比之下,對于一段用資源管理技術實作的代碼。你也用grep檢查所有的new,但是這次你隻需要檢查鄰近的調用:

  ● 這是一個直接的strong pointer轉換,還是我們在一個構造函數的函數體中?

  ● 調用的傳回知是否立即儲存到對象中,構造函數中是否有可以産生異常的代碼。?

  ● 如果這樣的話析構函數中時候有delete?

  下一步,你需要用grep查找所有的release方法,并實施相同的檢查。

  不同點是需要檢查、了解單個執行路徑和隻需要做一些本地的檢驗。這難道不是提醒你非結構化的和結構化的程式設計的不同嗎?原理上,你可以認為你可以應付goto,并且跟蹤所有的可能分支。另一方面,你可以将你的懷疑本地化為一段代碼。本地化在兩種情況下都是關鍵所在。

  在資源管理中的錯誤模式也比較容易調試。最常見的bug是試圖通路一個釋放過的strong pointer。這将導緻一個錯誤,并且很容易跟蹤。

1.2.9 共享的所有權

  為每一個程式中的資源都找出或者指定一個所有者是一件很容易的事情嗎?答案是出乎意料的,是!如果你發現了一些問題,這可能說明你的設計上存在問題。還有另一種情況就是共享所有權是最好的甚至是唯一的選擇。

  共享的責任配置設定給被共享的對象和它的客戶(client)。一個共享資源必須為它的所有者保持一個引用計數。另一方面,所有者再釋放資源的時候必須通報共享對象。最後一個釋放資源的需要在最後負責free的工作。

  最簡單的共享的實作是共享對象繼承引用計數的類refcounted:

refcounted

refcounted () : _count (1) {}

getrefcount () const { return _count; }

void increfcount () { _count++;

int decrefcount () { return --_count; }

_count;

  按照資源管理,一個引用計數是一種資源。如果你遵守它,你需要釋放它。當你意識到這一事實的時候,剩下的就變得簡單了。簡單的遵循規則--再構造函數中獲得引用計數,在析構函數中釋放。甚至有一個refcounted的smart

pointer等價物:

refptr

refptr (t * p) : _p (p) {}

(refptr<t> & p)

_p = p._p;

_p->increfcount

~refptr ()

if (_p->decrefcount () == 0)

delete

_p;

t * _p;

  注意模闆中的t不比成為refcounted的後代,但是它必須有increfcount和decrefcount的方法。當然,一個便于使用的refptr需要有一個重載的指針通路操作符。在refptr中加入轉換語義學(transfer

semantics)是讀者的工作。

1.2.10 所有權網絡

  連結清單是資源管理分析中的一個很有意思的例子。如果你選擇表成為鍊(link)的所有者的話,你會陷入實作遞歸的所有權。每一個link都是它的繼承者的所有者,并且,相應的,餘下的連結清單的所有者。下面是用smart

pointer實作的一個表單元:

class link

auto_ptr<link>

_next;

  最好的方法是,将連接配接控制封裝到一個弄構進行資源轉換的類中。

  對于雙連結清單呢?安全的做法是指明一個方向,如forward:

doublelink

// ...

*_prev;

auto_ptr<doublelink> _next;

  注意不要建立環形連結清單。

  這給我們帶來了另外一個有趣的問題--資源管理可以處理環形的所有權嗎?它可以,用一個mark-and-sweep的算法。這裡是實作這種方法的一個例子:

cyclptr

cyclptr (t * p)

:_p (p), _isbeingdeleted

(false)

~cyclptr ()

_isbeingdeleted = true;

if

(!_p->isbeingdeleted ())

void set (t *

p)

_p = p;

bool isbeingdeleted () const { return

_isbeingdeleted; }

bool

_isbeingdeleted;

  注意我們需要用class t來實作方法isbeingdeleted,就像從cyclptr繼承。對特殊的所有權網絡普通化是十分直接的。

  将原有代碼轉換為資源管理代碼

如果你是一個經驗豐富的程式員,你一定會知道找資源的bug是一件浪費時間的痛苦的經曆。我不必說服你和你的團隊花費一點時間來熟悉資源管理是十分值得的。你可以立即開始用這個方法,無論你是在開始一個新項目或者是在一個項目的中期。轉換不必立即全部完成。下面是步驟。

(1)       首先,在你的工程中建立基本的strong

pointer。然後通過查找代碼中的new來開始封裝裸指針。

(2)      

最先封裝的是在過程中定義的臨時指針。簡單的将它們替換為auto_ptr并且删除相應的delete。如果一個指針在過程中沒有被删除而是被傳回,用auto_ptr替換并在傳回前調用release方法。在你做第二次傳遞的時候,你需要處理對release的調用。注意,即使是在這點,你的代碼也可能更加"精力充沛"--你會移出代碼中潛在的資源洩漏問題。

(3)      

下面是指向資源的裸指針。確定它們被獨立的封裝到auto_ptr中,或者在構造函數中配置設定在析構函數中釋放。如果你有傳遞所有權的行為的話,需要調用release方法。如果你有容器所有對象,用strong

pointers重新實作它們。

(4)      

接下來,找到所有對release的方法調用并且盡力清除所有,如果一個release調用傳回一個指針,将它修改傳值傳回一個auto_ptr。

(5)      

重複着一過程,直到最後所有new和release的調用都在構造函數或者資源轉換的時候發生。這樣,你在你的代碼中處理了資源洩漏的問題。對其他資源進行相似的操作。

(6)      

你會發現資源管理清除了許多錯誤和異常處理帶來的複雜性。不僅僅你的代碼會變得精力充沛,它也會變得簡單并容易維護。

2 記憶體洩漏

2.1 c++中動态記憶體配置設定引發問題的解決方案

假設我們要開發一個string類,它可以友善地處理字元串資料。我們可以在類中聲明一個數組,考慮到有時候字元串極長,我們可以把數組大小設為200,但一般的情況下又不需要這麼多的空間,這樣是浪費了記憶體。對了,我們可以使用new操作符,這樣是十分靈活的,但在類中就會出現許多意想不到的問題,本文就是針對這一現象而寫的。現在,我們先來開發一個string類,但它是一個不完善的類。的确,我們要刻意地使它出現各種各樣的問題,這樣才好對症下藥。好了,我們開始吧!

/* string.h */

#ifndef

string_h_

#define string_h_

class string

private:

char *

str; //存儲資料

int len; //字元串長度

string(const char * s);

//構造函數

string(); // 預設構造函數

~string(); // 析構函數

friend ostream

& operator<<(ostream & os,const string&

st);

#endif

/*string.cpp*/

#include <iostream>

#include

<cstring>

#include "string.h"

using namespace

std;

string::string(const char * s)

len = strlen(s);

str =

new char[len + 1];

strcpy(str,

s);

}//拷貝資料

string::string()

len =0;

str = new

char[len+1];

str[0]=‘"0‘;

string::~string()

cout<<"這個字元串将被删除:"<<str<<‘"n‘;//為了友善觀察結果,特留此行代碼。

[] str;

ostream & operator<<(ostream & os, const string

& st)

os << st.str;

os;

/*test_right.cpp*/

<stdlib.h>

using namespace std;

main()

string

temp("天極網");

cout<<temp<<‘"n‘;

system("pause");

  運作結果:

  天極網

  請按任意鍵繼續. . .

  大家可以看到,以上程式十分正确,而且也是十分有用的。可是,我們不能被表面現象所迷惑!下面,請大家用test_string.cpp檔案替換test_right.cpp檔案進行編譯,看看結果。有的編譯器可能就是根本不能進行編譯!

test_string.cpp:

show_right(const string&);

void show_string(const

string);//注意,參數非引用,而是按值傳遞。

int main()

test1("第一個範例。");

string test2("第二個範例。");

test3("第三個範例。");

test4("第四個範例。");

cout<<"下面分别輸入三個範例:"n";

cout<<test1<<endl;

cout<<test2<<endl;

cout<<test3<<endl;

string*

string1=new string(test1);

cout<<*string1<<endl;

string1;

//在dev-cpp上沒有任何反應。

cout<<"使用正确的函數:"<<endl;

show_right(test2);

cout<<"使用錯誤的函數:"<<endl;

show_string(test2);

//這一段代碼出現嚴重的錯誤!

string string2(test3);

cout<<"string2:

"<<string2<<endl;

string string3;

string3=test4;

cout<<"string3:

"<<string3<<endl;

cout<<"下面,程式結束,析構函數将被調用。"<<endl;

void show_right(const string&

a)

cout<<a<<endl;

void show_string(const string

  下面分别輸入三個範例:

  第一個範例。

  第二個範例。

  第三個範例。

  這個字元串将被删除:第一個範例。

  使用正确的函數:

  使用錯誤的函數:

  這個字元串将被删除:第二個範例。

  這個字元串将被删除:?=

  ?=

  string2:

第三個範例。

  string3:

第四個範例。

  下面,程式結束,析構函數将被調用。

  這個字元串将被删除:第四個範例。

  這個字元串将被删除:第三個範例。

  這個字元串将被删除:x

=

  這個字元串将被删除:

現在,請大家自己試試運作結果,或許會更加慘不忍睹呢!下面,我為大家一一分析原因。

首先,大家要知道,c++類有以下這些極為重要的函數:

一:複制構造函數。

二:指派函數。

我們先來講複制構造函數。什麼是複制構造函數呢?比如,我們可以寫下這樣的代碼:string

test1(test2);這是進行初始化。我們知道,初始化對象要用構造函數。可這兒呢?按理說,應該有聲明為這樣的構造函數:string(const string

&);可是,我們并沒有定義這個構造函數呀?答案是,c++提供了預設的複制構造函數,問題也就出在這兒。

(1):什麼時候會調用複制構造函數呢?(以string類為例。)

  在我們提供這樣的代碼:string test1(test2)時,它會被調用;當函數的參數清單為按值傳遞,也就是沒有用引用和指針作為類型時,如:void

show_string(const string),它會被調用。其實,還有一些情況,但在這兒就不列舉了。

(2):它是什麼樣的函數。

它的作用就是把兩個類進行複制。拿string類為例,c++提供的預設複制構造函數是這樣的:

string(const string&

str=a.str;

len=a.len;

在平時,這樣并不會有任何的問題出現,但我們用了new操

作符,涉及到了動态記憶體配置設定,我們就不得不談談淺複制和深複制了。以上的函數就是實行的淺複制,它隻是複制了指針,而并沒有複制指針指向的資料,可謂一點

兒用也沒有。打個比方吧!就像一個朋友讓你把一個程式通過網絡發給他,而你大大咧咧地把快捷方式發給了他,有什麼用處呢?我們來具體談談:

假如,a對象中存儲了這樣的字元串:“c++”。它的位址為2000。現在,我們把a對象賦給b對象:string

b=a。現在,a和b對象的str指針均指向2000位址。看似可以使用,但如果b對象的析構函數被調用時,則位址2000處的字元串“c++”已經被從記憶體中抹去,而a對象仍然指向位址2000。這時,如果我們寫下這樣的代碼:cout<<a<<endl;或是等待程式結束,a對象的析構函數被調用時,a對象的資料能否顯示出來呢?隻會是亂碼。而且,程式還會這樣做:連續對位址2000處使用兩次delete操作符,這樣的後果是十分嚴重的!

本例中,有這樣的代碼:

string* string1=new

string(test1);

  假設test1中str指向的位址為2000,而string中str指針同樣指向位址2000,我們删除了2000處的資料,而test1對象呢?已經被破壞了。大家從運作結果上可以看到,我們使用cout<<test1時,一點反應也沒有。而在test1的析構函數被調用時,顯示是這樣:“這個字元串将被删除:”。

再看看這段代碼:

cout<<test2<<endl;//這一段代碼出現嚴重的錯誤!

show_string函數的參數清單void show_string(const string

a)是按值傳遞的,是以,我們相當于執行了這樣的代碼:string

a=test2;函數執行完畢,由于生存周期的緣故,對象a被析構函數删除,我們馬上就可以看到錯誤的顯示結果了:這個字元串将被删除:?=。當然,test2也被破壞了。解決的辦法很簡單,當然是手工定義一個複制構造函數喽!人力可以勝天!

string::string(const string&

str=new

char(len+1);

strcpy(str,a.str);

  我們執行的是深複制。這個函數的功能是這樣的:假設對象a中的str指針指向位址2000,内容為“i am a c++

boy!”。我們執行代碼string

b=a時,我們先開辟出一塊記憶體,假設為3000。我們用strcpy函數将位址2000的内容拷貝到位址3000中,再将對象b的str指針指向位址3000。這樣,就互不幹擾了。

大家把這個函數加入程式中,問題就解決了大半,但還沒有完全解決,問題在指派函數上。我們的程式中有這樣的段代碼:

string3;

  經過我前面的講解,大家應該也會對這段代碼進行尋根摸底:憑什麼可以這樣做:string3=test4???原因是,c++為了使用者的友善,提供的這樣的一個操作符重載函數:operator=。是以,我們可以這樣做。大家應該猜得到,它同樣是執行了淺複制,出了同樣的毛病。比如,執行了這段代碼後,析構函數開始大展神威^_^。由于這些變量是後進先出的,是以最後的string3變量先被删除:這個字元串将被删除:第四個範例。很正常。最後,删除到test4的時候,問題來了:這個字元串将被删除:?=。原因我不用贅述了,隻是這個指派函數怎麼寫,還有一點兒學問呢!大家請看:

平時,我們可以寫這樣的代碼:x=y=z。(均為整型變量。)而在類對象中,我們同樣要這樣,因為這很友善。而對象a=b=c就是a.operator=(b.operator=(c))。而這個operator=函數的參數清單應該是:const

string& a,是以,大家不難推出,要實作這樣的功能,傳回值也要是string&,這樣才能實作a=b=c。我們先來寫寫看:

string& string::operator=(const

string& a)

delete [] str;//先删除自身的資料

strcpy(str,a.str);//此三行為進行拷貝

*this;//傳回自身的引用

是不是這樣就行了呢?我們假如寫出了這種代碼:a=a,那麼大家看看,豈不是把a對象的資料給删除了嗎?這樣可謂引發一系列的錯誤。是以,我們還要檢查是否為自身指派。隻比較兩對象的資料是不行了,因為兩個對象的資料很有可能相同。我們應該比較位址。以下是完好的指派函數:

if(this==&a)

return *this;

delete []

str;

str=new char[len+1];

*this;

把這些代碼加入程式,問題就完全解決,下面是運作結果:

  第一個範例

  第二個範例

  第三個範例

   使用正确的函數:

   使用錯誤的函數:

2.2 如何對付記憶體洩漏?

寫出那些不會導緻任何記憶體洩漏的代碼。很明顯,當你的代碼中到處充滿了new

操作、delete操作和指針運算的話,你将會在某個地方搞暈了頭,導緻記憶體洩漏,指針引用錯誤,以及諸如此類的問題。這和你如何小心地對待記憶體配置設定工作其實完全沒有關系:代碼的複雜性最終總是會超過你能夠付出的時間和努力。于是随後産生了一些成功的技巧,它們依賴于将記憶體配置設定(allocations)與重新配置設定(deallocation)工作隐藏在易于管理的類型之後。标準容器(standard

containers)是一個優秀的例子。它們不是通過你而是自己為元素管理記憶體,進而避免了産生糟糕的結果。想象一下,沒有string和vector的幫助,寫出這個:

#include<vector>

#include<string>

#include<iostream>

#include<algorithm>

using

namespace std;

int main() // small program messing around with

strings

 cout << "enter some whitespace-separated

words:"n";

 vector<string> v;

 string s;

 while

(cin>>s) v.push_back(s);

 sort(v.begin(),v.end());

 string

cat;

 typedef vector<string>::const_iterator iter;

 for (iter

p = v.begin(); p!=v.end(); ++p) cat += *p+"+";

 cout << cat

<< ’"n’;

  你有多少機會在第一次就得到正确的結果?你又怎麼知道你沒有導緻記憶體洩漏呢?

  注意,沒有出現顯式的記憶體管理,宏,造型,溢出檢查,顯式的長度限制,以及指針。通過使用函數對象和标準算法(standard

algorithm),我可以避免使用指針——例如使用疊代子(iterator),不過對于一個這麼小的程式來說有點小題大作了。

  這些技巧并不完美,要系統化地使用它們也并不總是那麼容易。但是,應用它們産生了驚人的差異,而且通過減少顯式的記憶體配置設定與重新配置設定的次數,你甚至可以使餘下的例子更加容易被跟蹤。早在1981年,我就指出,通過将我必須顯式地跟蹤的對象的數量從幾萬個減少到幾打,為了使程式正确運作而付出的努力從可怕的苦工,變成了應付一些可管理的對象,甚至更加簡單了。

  如果你的程式還沒有包含将顯式記憶體管理減少到最小限度的庫,那麼要讓你程式完成和正确運作的話,最快的途徑也許就是先建立一個這樣的庫。

  模闆和标準庫實作了容器、資源句柄以及諸如此類的東西,更早的使用甚至在多年以前。異常的使用使之更加完善。

  如果你實在不能将記憶體配置設定/重新配置設定的操作隐藏到你需要的對象中時,你可以使用資源句柄(resource

handle),以将記憶體洩漏的可能性降至最低。這裡有個例子:我需要通過一個函數,在空閑記憶體中建立一個對象并傳回它。這時候可能忘記釋放這個對象。畢竟,我們不能說,僅僅關注當這個指針要被釋放的時候,誰将負責去做。使用資源句柄,這裡用了标準庫中的auto_ptr,使需要為之負責的地方變得明确了。

#include<memory>

struct s {

 s() { cout << "make an s"n";

 ~s() { cout << "destroy an s"n"; }

 s(const s&) { cout

<< "copy initialize an s"n"; }

 s& operator=(const s&) {

cout << "copy assign an s"n"; }

s* f()

 return new

s; // 誰該負責釋放這個s?

auto_ptr<s> g()

 return

auto_ptr<s>(new s); // 顯式傳遞負責釋放這個s

 cout

<< "start main"n";

 s* p = f();

 cout << "after f()

before g()"n";

 // s* q = g(); // 将被編譯器捕捉

 auto_ptr<s> q =

g();

 cout << "exit main"n";

 // *p産生了記憶體洩漏

 //

*q被自動釋放

  在更一般的意義上考慮資源,而不僅僅是記憶體。

如果在你的環境中不能系統地應用這些技巧(例如,你必須使用别的地方的代碼,或者你的程式的另一部分簡直是原始人類(譯注:原文是neanderthals,尼安德特人,舊石器時代廣泛分布在歐洲的猿人)寫的,如此等等),那麼注意使用一個記憶體洩漏檢測器作為開發過程的一部分,或者插入一個垃圾收集器(garbage

collector)。

2.3淺談c/c++記憶體洩漏及其檢測工具

  對于一個c/c++程式員來說,記憶體洩漏是一個常見的也是令人頭疼的問題。已經有許多技術被研究出來以應對這個問題,比如smart

pointer,garbage collection等。smart pointer技術比較成熟,stl中已經包含支援smart

pointer的class,但是它的使用似乎并不廣泛,而且它也不能解決所有的問題;garbage

collection技術在java中已經比較成熟,但是在c/c++領域的發展并不順暢,雖然很早就有人思考在c++中也加入gc的支援。現實世界就是這樣的,作為一個c/c++程式員,記憶體洩漏是你心中永遠的痛。不過好在現在有許多工具能夠幫助我們驗證記憶體洩漏的存在,找出發生問題的代碼。

2.3.1 記憶體洩漏的定義

一般我們常說的記憶體洩漏是指堆記憶體的洩漏。堆記憶體是指程式從堆中配置設定的,大小任意的(記憶體塊的大小可以在程式運作期決定),使用完後必須顯示釋放的記憶體。應用程式一般使用malloc,realloc,new等函數從堆中配置設定到一塊記憶體,使用完後,程式必須負責相應的調用free或delete釋放該記憶體塊,否則,這塊記憶體就不能被再次使用,我們就說這塊記憶體洩漏了。以下這段小程式示範了堆記憶體發生洩漏的情形:

void myfunction(int nsize)

 char* p=

new char[nsize];

 if( !getstringfrom( p, nsize )

){

  messagebox(“error”);

 …//using the string

pointed by p;

 delete p;

  當函數getstringfrom()傳回零的時候,指針p指向的記憶體就不會被釋放。這是一種常見的發生記憶體洩漏的情形。程式在入口處配置設定記憶體,在出口處釋放記憶體,但是c函數可以在任何地方退出,是以一旦有某個出口處沒有釋放應該釋放的記憶體,就會發生記憶體洩漏。

  廣義的說,記憶體洩漏不僅僅包含堆記憶體的洩漏,還包含系統資源的洩漏(resource leak),比如核心态handle,gdi

object,socket,

interface等,從根本上說這些由作業系統配置設定的對象也消耗記憶體,如果這些對象發生洩漏最終也會導緻記憶體的洩漏。而且,某些對象消耗的是核心态記憶體,這些對象嚴重洩漏時會導緻整個作業系統不穩定。是以相比之下,系統資源的洩漏比堆記憶體的洩漏更為嚴重。

gdi object的洩漏是一種常見的資源洩漏:

void cmyview::onpaint( cdc* pdc

)

 cbitmap bmp;

 cbitmap*

poldbmp;

 bmp.loadbitmap(idb_mybmp);

 poldbmp =

pdc->selectobject( &bmp );

 if( something()

 pdc->selectobject( poldbmp

 return;

  當函數something()傳回非零的時候,程式在退出前沒有把poldbmp選回pdc中,這會導緻poldbmp指向的hbitmap對象發生洩漏。這個程式如果長時間的運作,可能會導緻整個系統花屏。這種問題在win9x下比較容易暴露出來,因為win9x的gdi堆比win2k或nt的要小很多。

2.3.2 記憶體洩漏的發生方式

  以發生的方式來分類,記憶體洩漏可以分為4類:

  1.

常發性記憶體洩漏。發生記憶體洩漏的代碼會被多次執行到,每次被執行的時候都會導緻一塊記憶體洩漏。比如例二,如果something()函數一直傳回true,那麼poldbmp指向的hbitmap對象總是發生洩漏。

  2.

偶發性記憶體洩漏。發生記憶體洩漏的代碼隻有在某些特定環境或操作過程下才會發生。比如例二,如果something()函數隻有在特定環境下才傳回true,那麼poldbmp指向的hbitmap對象并不總是發生洩漏。常發性和偶發性是相對的。對于特定的環境,偶發性的也許就變成了常發性的。是以測試環境和測試方法對檢測記憶體洩漏至關重要。

3.

一次性記憶體洩漏。發生記憶體洩漏的代碼隻會被執行一次,或者由于算法上的缺陷,導緻總會有一塊僅且一塊記憶體發生洩漏。比如,在類的構造函數中配置設定記憶體,在析構函數中卻沒有釋放該記憶體,但是因為這個類是一個singleton,是以記憶體洩漏隻會發生一次。另一個例子:

char* g_lpszfilename = null;

setfilename( const char* lpcszfilename )

 if( g_lpszfilename

  free( g_lpszfilename );

 g_lpszfilename = strdup(

lpcszfilename );

  如果程式在結束的時候沒有釋放g_lpszfilename指向的字元串,那麼,即使多次調用setfilename(),總會有一塊記憶體,而且僅有一塊記憶體發生洩漏。

4.

隐式記憶體洩漏。程式在運作過程中不停的配置設定記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡并沒有發生記憶體洩漏,因為最終程式釋放了所有申請的記憶體。但是對于一個伺服器程式,需要運作幾天,幾周甚至幾個月,不及時釋放記憶體也可能導緻最終耗盡系統的所有記憶體。是以,我們稱這類記憶體洩漏為隐式記憶體洩漏。舉一個例子:

connection

  connection( socket

  ~connection();

  socket

_socket;

connectionmanager

  connectionmanager(){}

  ~connectionmanager(){

   list::iterator

it;

   for( it = _connlist.begin(); it != _connlist.end(); ++it

    delete (*it);

   }

   _connlist.clear();

onclientconnected( socket s ){

   connection* p = new

connection(s);

   _connlist.push_back(p);

onclientdisconnected( connection* pconn ){

   _connlist.remove( pconn

   delete pconn;

  list

_connlist;

  假設在client從server端斷開後,server并沒有呼叫onclientdisconnected()函數,那麼代表那次連接配接的connection對象就不會被及時的删除(在server程式退出的時候,所有connection對象會在connectionmanager的析構函數裡被删除)。當不斷的有連接配接建立、斷開時隐式記憶體洩漏就發生了。

從使用者使用程式的角度來看,記憶體洩漏本身不會産生什麼危害,作為一般的使用者,根本感覺不到記憶體洩漏的存在。真正有危害的是記憶體洩漏的堆積,這會最終消耗盡系統所有的記憶體。從這個角度來說,一次性記憶體洩漏并沒有什麼危害,因為它不會堆積,而隐式記憶體洩漏危害性則非常大,因為較之于常發性和偶發性記憶體洩漏它更難被檢測到。

2.3.3 檢測記憶體洩漏

  檢測記憶體洩漏的關鍵是要能截獲住對配置設定記憶體和釋放記憶體的函數的調用。截獲住這兩個函數,我們就能跟蹤每一塊記憶體的生命周期,比如,每當成功的配置設定一塊記憶體後,就把它的指針加入一個全局的list中;每當釋放一塊記憶體,再把它的指針從list中删除。這樣,當程式結束的時候,list中剩餘的指針就是指向那些沒有被釋放的記憶體。這裡隻是簡單的描述了檢測記憶體洩漏的基本原理,詳細的算法可以參見steve

maguire的<<writing solid code>>。

  如果要檢測堆記憶體的洩漏,那麼需要截獲住malloc/realloc/free和new/delete就可以了(其實new/delete最終也是用malloc/free的,是以隻要截獲前面一組即可)。對于其他的洩漏,可以采用類似的方法,截獲住相應的配置設定和釋放函數。比如,要檢測bstr的洩漏,就需要截獲sysallocstring/sysfreestring;要檢測hmenu的洩漏,就需要截獲createmenu/

destroymenu。(有的資源的配置設定函數有多個,釋放函數隻有一個,比如,sysallocstringlen也可以用來配置設定bstr,這時就需要截獲多個配置設定函數)

  在windows平台下,檢測記憶體洩漏的工具常用的一般有三種,ms c-runtime

library内建的檢測功能;外挂式的檢測工具,諸如,purify,boundschecker等;利用windows nt自帶的performance

monitor。這三種工具各有優缺點,ms c-runtime library雖然功能上較之外挂式的工具要弱,但是它是免費的;performance

monitor雖然無法标示出發生問題的代碼,但是它能檢測出隐式的記憶體洩漏的存在,這是其他兩類工具無能為力的地方。

  以下我們詳細讨論這三種檢測工具:

2.3.3.1 vc下記憶體洩漏的檢測方法

  用mfc開發的應用程式,在debug版模式下編譯後,都會自動加入記憶體洩漏的檢測代碼。在程式結束後,如果發生了記憶體洩漏,在debug視窗中會顯示出所有發生洩漏的記憶體塊的資訊,以下兩行顯示了一塊被洩漏的記憶體塊的資訊:

e:"testmemleak"testdlg.cpp(70) : {59} normal block at 0x00881710, 200 bytes

long.

data: <abcdefghijklmnop> 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f

70

  第一行顯示該記憶體塊由testdlg.cpp檔案,第70行代碼配置設定,位址在0x00881710,大小為200位元組,{59}是指調用記憶體配置設定函數的request

order,關于它的詳細資訊可以參見msdn中_crtsetbreakalloc()的幫助。第二行顯示該記憶體塊前16個位元組的内容,尖括号内是以ascii方式顯示,接着的是以16進制方式顯示。

  一般大家都誤以為這些記憶體洩漏的檢測功能是由mfc提供的,其實不然。mfc隻是封裝和利用了ms c-runtime library的debug

function。非mfc程式也可以利用ms c-runtime library的debug function加入記憶體洩漏的檢測功能。ms c-runtime

library在實作malloc/free,strdup等函數時已經内建了記憶體洩漏的檢測功能。

注意觀察一下由mfc application wizard生成的項目,在每一個cpp檔案的頭部都有這樣一段宏定義:

#ifdef _debug

#define new

debug_new

#undef this_file

static char this_file[] =

__file__;

有了這樣的定義,在編譯debug版時,出現在這個cpp檔案中的所有new都被替換成debug_new了。那麼debug_new是什麼呢?debug_new也是一個宏,以下摘自afx.h,1632行

#define debug_new new(this_file,

__line__)

是以如果有這樣一行代碼:

char* p = new char[200];

經過宏替換就變成了:

char* p = new( this_file,

__line__)char[200];

根據c++的标準,對于以上的new的使用方法,編譯器會去找這樣定義的operator new:

void* operator new(size_t, lpcstr,

int)

我們在afxmem.cpp 63行找到了一個這樣的operator new 的實作

void* afx_cdecl operator new(size_t nsize,

lpcstr lpszfilename, int nline)

 return ::operator new(nsize,

_normal_block, lpszfilename, nline);

void* __cdecl operator

new(size_t nsize, int ntype, lpcstr lpszfilename, int

nline)

 presult = _malloc_dbg(nsize, ntype, lpszfilename,

nline);

 if (presult != null)

  return

presult;

  第二個operator new函數比較長,為了簡單期間,我隻摘錄了部分。很顯然最後的記憶體配置設定還是通過_malloc_dbg函數實作的,這個函數屬于ms

c-runtime library 的debug

function。這個函數不但要求傳入記憶體的大小,另外還有檔案名和行号兩個參數。檔案名和行号就是用來記錄此次配置設定是由哪一段代碼造成的。如果這塊記憶體在程式結束之前沒有被釋放,那麼這些資訊就會輸出到debug視窗裡。

  這裡順便提一下this_file,__file和__line__。__file__和__line__都是編譯器定義的宏。當碰到__file__時,編譯器會把__file__替換成一個字元串,這個字元串就是目前在編譯的檔案的路徑名。當碰到__line__時,編譯器會把__line__替換成一個數字,這個數字就是目前這行代碼的行号。在debug_new的定義中沒有直接使用__file__,而是用了this_file,其目的是為了減小目标檔案的大小。假設在某個cpp檔案中有100處使用了new,如果直接使用__file__,那編譯器會産生100個常量字元串,這100個字元串都是飧?/span>cpp檔案的路徑名,顯然十分備援。如果使用this_file,編譯器隻會産生一個常量字元串,那100處new的調用使用的都是指向常量字元串的指針。

  再次觀察一下由mfc application

wizard生成的項目,我們會發現在cpp檔案中隻對new做了映射,如果你在程式中直接使用malloc函數配置設定記憶體,調用malloc的檔案名和行号是不會被記錄下來的。如果這塊記憶體發生了洩漏,ms

c-runtime library仍然能檢測到,但是當輸出這塊記憶體塊的資訊,不會包含配置設定它的的檔案名和行号。

要在非mfc程式中打開記憶體洩漏的檢測功能非常容易,你隻要在程式的入口處加入以下幾行代碼:

int tmpflag = _crtsetdbgflag(

_crtdbg_report_flag );

tmpflag |=

_crtdbg_leak_check_df;

_crtsetdbgflag( tmpflag );

  這樣,在程式結束的時候,也就是winmain,main或dllmain函數傳回之後,如果還有記憶體塊沒有釋放,它們的資訊會被列印到debug視窗裡。

如果你試着建立了一個非mfc應用程式,而且在程式的入口處加入了以上代碼,并且故意在程式中不釋放某些記憶體塊,你會在debug視窗裡看到以下的資訊:

{47} normal block at 0x00c91c90, 200 bytes

data: < > 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e

0f

  記憶體洩漏的确檢測到了,但是和上面mfc程式的例子相比,缺少了檔案名和行号。對于一個比較大的程式,沒有這些資訊,解決問題将變得十分困難。

  為了能夠知道洩漏的記憶體塊是在哪裡配置設定的,你需要實作類似mfc的映射功能,把new,maolloc等函數映射到_malloc_dbg函數上。這裡我不再贅述,你可以參考mfc的源代碼。

  由于debug function實作在ms

c-runtimelibrary中,是以它隻能檢測到堆記憶體的洩漏,而且隻限于malloc,realloc或strdup等配置設定的記憶體,而那些系統資源,比如handle,gdi

object,或是不通過c-runtime

library配置設定的記憶體,比如variant,bstr的洩漏,它是無法檢測到的,這是這種檢測法的一個重大的局限性。另外,為了能記錄記憶體塊是在哪裡配置設定的,源代碼必須相應的配合,這在調試一些老的程式非常麻煩,畢竟修改源代碼不是一件省心的事,這是這種檢測法的另一個局限性。

對于開發一個大型的程式,ms c-runtime

library提供的檢測功能是遠遠不夠的。接下來我們就看看外挂式的檢測工具。我用的比較多的是boundschecker,一則因為它的功能比較全面,更重要的是它的穩定性。這類工具如果不穩定,反而會忙裡添亂。到底是出自鼎鼎大名的numega,我用下來基本上沒有什麼大問題。

2.3.3.2 使用boundschecker檢測記憶體洩漏

  boundschecker采用一種被稱為 code

injection的技術,來截獲對配置設定記憶體和釋放記憶體的函數的調用。簡單地說,當你的程式開始運作時,boundschecker的dll被自動載入程序的位址空間(這可以通過system-level的hook實作),然後它會修改程序中對記憶體配置設定和釋放的函數調用,讓這些調用首先轉入它的代碼,然後再執行原來的代碼。boundschecker在做這些動作的時,無須修改被調試程式的源代碼或工程配置檔案,這使得使用它非常的簡便、直接。

  這裡我們以malloc函數為例,截獲其他的函數方法與此類似。

  需要被截獲的函數可能在dll中,也可能在程式的代碼裡。比如,如果靜态連結c-runtime

library,那麼malloc函數的代碼會被連結到程式裡。為了截獲住對這類函數的調用,boundschecker會動态修改這些函數的指令。

以下兩段彙編代碼,一段沒有boundschecker介入,另一段則有boundschecker的介入:

126: _crtimp void * __cdecl malloc (

127:

size_t nsize

128: )

129: {

00403c10 push ebp

00403c11 mov

ebp,esp

130: return _nh_malloc_dbg(nsize, _newmode, _normal_block,

null, 0);

00403c13 push 0

00403c15 push 0

00403c17 push

1

00403c19 mov eax,[__newmode (0042376c)]

00403c1e push

eax

00403c1f mov ecx,dword ptr [nsize]

00403c22 push ecx

00403c23

call _nh_malloc_dbg (00403c80)

00403c28 add esp,14h

131:

以下這一段代碼有boundschecker介入:

00403c10 jmp 01f41ec8

00403c15 push

00403c17 push 1

00403c19 mov eax,[__newmode

(0042376c)]

00403c1e push eax

00403c1f mov ecx,dword ptr

[nsize]

00403c23 call _nh_malloc_dbg

(00403c80)

131: }

  當boundschecker介入後,函數malloc的前三條彙編指令被替換成一條jmp指令,原來的三條指令被搬到位址01f41ec8處了。當程式進入malloc後先jmp到01f41ec8,執行原來的三條指令,然後就是boundschecker的天下了。大緻上它會先記錄函數的傳回位址(函數的傳回位址在stack上,是以很容易修改),然後把傳回位址指向屬于boundschecker的代碼,接着跳到malloc函數原來的指令,也就是在00403c15的地方。當malloc函數結束的時候,由于傳回位址被修改,它會傳回到boundschecker的代碼中,此時boundschecker會記錄由malloc配置設定的記憶體的指針,然後再跳轉到到原來的傳回位址去。

  如果記憶體配置設定/釋放函數在dll中,boundschecker則采用另一種方法來截獲對這些函數的調用。boundschecker通過修改程式的dll

import table讓table中的函數位址指向自己的位址,以達到截獲的目的。

截獲住這些配置設定和釋放函數,boundschecker就能記錄被配置設定的記憶體或資源的生命周期。接下來的問題是如何與源代碼相關,也就是說當boundschecker檢測到記憶體洩漏,它如何報告這塊記憶體塊是哪段代碼配置設定的。答案是調試資訊(debug

information)。當我們編譯一個debug版的程式時,編譯器會把源代碼和二進制代碼之間的對應關系記錄下來,放到一個單獨的檔案裡(.pdb)或者直接連結進目标程式,通過直接讀取調試資訊就能得到配置設定某塊記憶體的源代碼在哪個檔案,哪一行上。使用code

injection和debug information,使boundschecker不但能記錄呼叫配置設定函數的源代碼的位置,而且還能記錄配置設定時的call

stack,以及call stack上的函數的源代碼位置。這在使用像mfc這樣的類庫時非常有用,以下我用一個例子來說明:

void showxitemmenu()

 cmenu

menu;

 menu.createpopupmenu();

 //add menu

items.

 menu.trackpropupmenu();

void showyitemmenu(

 cmenu menu;

 menu.detach();//this will cause

hmenu leak

cmenu::createpopupmenu()

 hmenu =

createpopupmenu();

當調用showyitemmenu()時,我們故意造成hmenu的洩漏。但是,對于boundschecker來說被洩漏的hmenu是在class

cmenu::createpopupmenu()中配置設定的。假設的你的程式有許多地方使用了cmenu的createpopupmenu()函數,如cmenu::createpopupmenu()造成的,你依然無法确認問題的根結到底在哪裡,在showxitemmenu()中還是在showyitemmenu()中,或者還有其它的地方也使用了createpopupmenu()?有了call

stack的資訊,問題就容易了。boundschecker會如下報告洩漏的hmenu的資訊:

function

file

line

cmenu::createpopupmenu

e:"8168"vc98"mfc"mfc"include"afxwin1.inl

1009

showyitemmenu

e:"testmemleak"mytest.cpp

100

  這裡省略了其他的函數調用

  如此,我們很容易找到發生問題的函數是showyitemmenu()。當使用mfc之類的類庫程式設計時,大部分的api調用都被封裝在類庫的class裡,有了call

stack資訊,我們就可以非常容易的追蹤到真正發生洩漏的代碼。

  記錄call stack資訊會使程式的運作變得非常慢,是以預設情況下boundschecker不會記錄call

stack資訊。可以按照以下的步驟打開記錄call stack資訊的選項開關:

  1. 打開菜單:boundschecker|setting…

  2. 在error detection頁中,在error detection scheme的list中選擇custom

  3. 在category的combox中選擇 pointer and leak error check

  4. 鈎上report call stack複選框

  5. 點選ok

  基于code injection,boundschecker還提供了api parameter的校驗功能,memory over

run等功能。這些功能對于程式的開發都非常有益。由于這些内容不屬于本文的主題,是以不在此詳述了。

盡管boundschecker的功能如此強大,但是面對隐式記憶體洩漏仍然顯得蒼白無力。是以接下來我們看看如何用performance

monitor檢測記憶體洩漏。

2.3.3.3 使用performance monitor檢測記憶體洩漏

  nt的核心在設計過程中已經加入了系統監視功能,比如cpu的使用率,記憶體的使用情況,i/o操作的頻繁度等都作為一個個counter,應用程式可以通過讀取這些counter了解整個系統的或者某個程序的運作狀況。performance

monitor就是這樣一個應用程式。

  為了檢測記憶體洩漏,我們一般可以監視process對象的handle count,virutal bytes 和working

set三個counter。handle

count記錄了程序目前打開的handle的個數,監視這個counter有助于我們發現程式是否有handle洩漏;virtual

bytes記錄了該程序目前在虛位址空間上使用的虛拟記憶體的大小,nt的記憶體配置設定采用了兩步走的方法,首先,在虛位址空間上保留一段空間,這時作業系統并沒有配置設定實體記憶體,隻是保留了一段位址。然後,再送出這段空間,這時作業系統才會配置設定實體記憶體。是以,virtual

bytes一般總大于程式的working set。監視virutal bytes可以幫助我們發現一些系統底層的問題; working

set記錄了作業系統為程序已送出的記憶體的總量,這個值和程式申請的記憶體總量存在密切的關系,如果程式存在記憶體的洩漏這個值會持續增加,但是virtual

bytes卻是跳躍式增加的。

  監視這些counter可以讓我們了解程序使用記憶體的情況,如果發生了洩漏,即使是隐式記憶體洩漏,這些counter的值也會持續增加。但是,我們知道有問題卻不知道哪裡有問題,是以一般使用performance

monitor來驗證是否有記憶體洩漏,而使用boundschecker來找到和解決。

  當performance

monitor顯示有記憶體洩漏,而boundschecker卻無法檢測到,這時有兩種可能:第一種,發生了偶發性記憶體洩漏。這時你要確定使用performance

monitor和使用boundschecker時,程式的運作環境和操作方法是一緻的。第二種,發生了隐式的記憶體洩漏。這時你要重新審查程式的設計,然後仔細研究performance

monitor記錄的counter的值的變化圖,分析其中的變化和程式運作邏輯的關系,找到一些可能的原因。這是一個痛苦的過程,充滿了假設、猜想、驗證、失敗,但這也是一個積累經驗的絕好機會。

3 探讨c++記憶體回收

3.1 c++記憶體對象大會戰

  如果一個人自稱為程式高手,卻對記憶體一無所知,那麼我可以告訴你,他一定在吹牛。用c或c++寫

程式,需要更多地關注記憶體,這不僅僅是因為記憶體的配置設定是否合理直接影響着程式的效率和性能,更為主要的是,當我們操作記憶體的時候一不小心就會出現問題,而

且很多時候,這些問題都是不易發覺的,比如記憶體洩漏,比如懸挂指針。筆者今天在這裡并不是要讨論如何避免這些問題,而是想從另外一個角度來認識c++記憶體對象。

  我們知道,c++将記憶體劃分為三個邏輯區域:堆、棧和靜态存儲區。既然如此,我稱位于它們之中的對象分别為堆對象,棧對象以及靜态對象。那麼這些不同的記憶體對象有什麼差別了?堆對象和棧對象各有什麼優劣了?如何禁止建立堆對象或棧對象了?這些便是今天的主題。

3.1.1 基本概念

  先來看看棧。棧,一般用于存放局部變量或對象,如我們在函數定義中用類似下面語句聲明的對象:

type stack_object ; 

  stack_object便是一個棧對象,它的生命期是從定義點開始,當所在函數傳回時,生命結束。

  另外,幾乎所有的臨時對象都是棧對象。比如,下面的函數定義:

type fun(type object);

  這個函數至少産生兩個臨時對象,首先,參數是按值傳遞的,是以會調用拷貝構造函數生成一個臨時對象object_copy1

,在函數内部使用的不是使用的不是object,而是object_copy1,自然,object_copy1是一個棧對象,它在函數傳回時被釋放;還有這個函數是值傳回的,在函數傳回時,如果我們不考慮傳回值優化(nrv),那麼也會産生一個臨時對象object_copy2,這個臨時對象會在函數傳回後一段時間内被釋放。比如某個函數中有如下代碼:

type tt ,result ; //生成兩個棧對象

tt = fun(tt);

//函數傳回時,生成的是一個臨時對象object_copy2

  上面的第二個語句的執行情況是這樣的,首先函數fun傳回時生成一個臨時對象object_copy2 ,然後再調用指派運算符執行

tt = object_copy2 ;

//調用指派運算符

  看到了嗎?編譯器在我們毫無知覺的情況下,為我們生成了這麼多臨時對象,而生成這些臨時對象的時間和空間的開銷可能是很大的,是以,你也許明白了,為什麼對于“大”對象最好用const引用傳遞代替按值進行函數參數傳遞了。

  接下來,看看堆。堆,又叫自由存儲區,它是在程式執行的過程中動态配置設定的,是以它最大的特性就是動态性。在c++中,所有堆對象的建立和銷毀都要由程式員負責,是以,如果處理不好,就會發生記憶體問題。如果配置設定了堆對象,卻忘記了釋放,就會産生記憶體洩漏;而如果已釋放了對象,卻沒有将相應的指針置為null,該指針就是所謂的“懸挂指針”,再度使用此指針時,就會出現非法通路,嚴重時就導緻程式崩潰。

  那麼,c++中是怎樣配置設定堆對象的?唯一的方法就是用new(當然,用類malloc指令也可獲得c式堆記憶體),隻要使用new,就會在堆中配置設定一塊記憶體,并且傳回指向該堆對象的指針。

  再來看看靜态存儲區。所有的靜态對象、全局對象都于靜态存儲區配置設定。關于全局對象,是在main()函數執行前就配置設定好了的。其實,在main()函數中的顯示代碼執行之前,會調用一個由編譯器生成的_main()函數,而_main()函數會進行所有全局對象的的構造及初始化工作。而在main()函數結束之前,會調用由編譯器生成的exit函數,來釋放所有的全局對象。比如下面的代碼:

void main(void)

 … …//

顯式代碼

  實際上,被轉化成這樣:

 _main();

//隐式代碼,由編譯器産生,用以構造所有全局對象

 … … // 顯式代碼

 … …

 exit() ; //

隐式代碼,由編譯器産生,用以釋放所有全局對象

  是以,知道了這個之後,便可以由此引出一些技巧,如,假設我們要在main()函數執行之前做某些準備工作,那麼我們可以将這些準備工作寫到一個自定義的全局對象的構造函數中,這樣,在main()函數的顯式代碼執行之前,這個全局對象的構造函數會被調用,執行預期的動作,這樣就達到了我們的目的。

剛才講的是靜态存儲區中的全局對象,那麼,局部靜态對象了?局部靜态對象通常也是在函數中定義的,就像棧對象一樣,隻不過,其前面多了個static關鍵字。局部靜态對象的生命期是從其所在函數第一次被調用,更确切地說,是當第一次執行到該靜态對象的聲明代碼時,産生該靜态局部對象,直到整個程式結束時,才銷毀該對象。

  還有一種靜态對象,那就是它作為class的靜态成員。考慮這種情況時,就牽涉了一些較複雜的問題。

  第一個問題是class的靜态成員對象的生命期,class的靜态成員對象随着第一個class

object的産生而産生,在整個程式結束時消亡。也就是有這樣的情況存在,在程式中我們定義了一個class,該類中有一個靜态對象作為成員,但是在程式執行過程中,如果我們沒有建立任何一個該class

object,那麼也就不會産生該class所包含的那個靜态對象。還有,如果建立了多個class

object,那麼所有這些object都共享那個靜态對象成員。

  第二個問題是,當出現下列情況時:

 class base

  static

type s_object ;

class derived1 : public base / / 公共繼承

…// other data

class derived2 : public base / / 公共繼承

base example ;

derivde1 example1 ;

derivde2

example2 ;

example.s_object = …… ;

example1.s_object = ……

example2.s_object = …… ; 

  請注意上面标為黑體的三條語句,它們所通路的s_object是同一個對象嗎?答案是肯定的,它們的确是指向同一個對象,這聽起來不像是真的,是嗎?但這是事實,你可以自己寫段簡單的代碼驗證一下。我要做的是來解釋為什麼會這樣?

我們知道,當一個類比如derived1,從另一個類比如base繼承時,那麼,可以看作一個derived1對象中含有一個base型的對象,這就是一個subobject。一個derived1對象的大緻記憶體布局如下:

  讓我們想想,當我們将一個derived1型的對象傳給一個接受非引用base型參數的函數時會發生切割,那麼是怎麼切割的呢?相信現在你已經知道了,那就是僅僅取出了derived1型的對象中的subobject,而忽略了所有derived1自定義的其它資料成員,然後将這個subobject傳遞給函數(實際上,函數中使用的是這個subobject的拷貝)。

  所有繼承base類的派生類的對象都含有一個base型的subobject(這是能用base型指針指向一個derived1對象的關鍵所在,自然也是多态的關鍵了),而所有的subobject和所有base型的對象都共用同一個s_object對象,自然,從base類派生的整個繼承體系中的類的執行個體都會共用同一個s_object對象了。上面提到的example、example1、example2的對象布局如下圖所示:

3.1.2 三種記憶體對象的比較

  棧對象的優勢是在适當的時候自動生成,又在适當的時候自動銷毀,不需要程式員操心;而且棧對象的建立速度一般較堆對象快,因為配置設定堆對象時,會調用operator

new操作,operator

new會采用某種記憶體空間搜尋算法,而該搜尋過程可能是很費時間的,産生棧對象則沒有這麼麻煩,它僅僅需要移動棧頂指針就可以了。但是要注意的是,通常棧空間容量比較小,一般是1mb~2mb,是以體積比較大的對象不适合在棧中配置設定。特别要注意遞歸函數中最好不要使用棧對象,因為随着遞歸調用深度的增加,所需的棧空間也會線性增加,當所需棧空間不夠時,便會導緻棧溢出,這樣就會産生運作時錯誤。

  堆對象,其産生時刻和銷毀時刻都要程式員精确定義,也就是說,程式員對堆對象的

生命具有完全的控制權。我們常常需要這樣的對象,比如,我們需要建立一個對象,能夠被多個函數所通路,但是又不想使其成為全局的,那麼這個時候建立一個堆

對象無疑是良好的選擇,然後在各個函數之間傳遞這個堆對象的指針,便可以實作對該對象的共享。另外,相比于棧空間,堆的容量要大得多。實際上,當實體記憶體

不夠時,如果這時還需要生成新的堆對象,通常不會産生運作時錯誤,而是系統會使用虛拟記憶體來擴充實際的實體記憶體。

接下來看看static對象。

  首先是全局對象。全局對象為類間通信和函數間通信提供了一種最簡單的方式,雖然這種方式并不優雅。一般而言,在完全的面向對象語言中,是不存在全局對象的,比如c#,因為全局對象意味着不安全和高耦合,在程式中過多地使用全局對象将大大降低程式的健壯性、穩定性、可維護性和可複用性。c++也完全可以剔除全局對象,但是最終沒有,我想原因之一是為了相容c。

  其次是類的靜态成員,上面已經提到,基類及其派生類的所有對象都共享這個靜态成員對象,是以當需要在這些class之間或這些class

objects之間進行資料共享或通信時,這樣的靜态成員無疑是很好的選擇。

  接着是靜态局部對象,主要可用于儲存該對象所在函數被屢次調用期間的中間狀态,其中一個最顯著的例子就是遞歸函數,我們都知道遞歸函數是自己調用自己的函數,如果在遞歸函數中定義一個nonstatic局部對象,那麼當遞歸次數相當大時,所産生的開銷也是巨大的。這是因為nonstatic局部對象是棧對象,每遞歸調用一次,就會産生一個這樣的對象,每傳回一次,就會釋放這個對象,而且,這樣的對象隻局限于目前調用層,對于更深入的嵌套層和更淺露的外層,都是不可見的。每個層都有自己的局部對象和參數。

  在遞歸函數設計中,可以使用static對象替代nonstatic局部對象(即棧對象),這不僅可以減少每次遞歸調用和傳回時産生和釋放nonstatic對象的開銷,而且static對象還可以儲存遞歸調用的中間狀态,并且可為各個調用層所通路。

3.1.3 使用棧對象的意外收獲

  前面已經介紹到,棧對象是在适當的時候建立,然後在适當的時候自動釋放的,也就

是棧對象有自動管理功能。那麼棧對象會在什麼會自動釋放了?第一,在其生命期結束的時候;第二,在其所在的函數發生異常的時候。你也許說,這些都很正常

啊,沒什麼大不了的。是的,沒什麼大不了的。但是隻要我們再深入一點點,也許就有意外的收獲了。

  棧對象,自動釋放時,會調用它自己的析構函數。如果我們在棧對象中封裝資源,而

且在棧對象的析構函數中執行釋放資源的動作,那麼就會使資源洩漏的機率大大降低,因為棧對象可以自動的釋放資源,即使在所在函數發生異常的時候。實際的過

程是這樣的:函數抛出異常時,會發生所謂的stack_unwinding(堆

棧復原),即堆棧會展開,由于是棧對象,自然存在于棧中,是以在堆棧復原的過程中,棧對象的析構函數會被執行,進而釋放其所封裝的資源。除非,除非在析構

函數執行的過程中再次抛出異常――而這種可能性是很小的,是以用棧對象封裝資源是比較安全的。基于此認識,我們就可以建立一個自己的句柄或代理來封裝資源

了。智能指針(auto_ptr)中就使用了這種技術。在有這種需要的時候,我們就希望我們的資源封裝類隻能在棧中建立,也就是要限制在堆中建立該資源封裝類的執行個體。

3.1.4 禁止産生堆對象

  上面已經提到,你決定禁止産生某種類型的堆對象,這時你可以自己建立一個資源封裝類,該類對象隻能在棧中産生,這樣就能在異常的情況下自動釋放封裝的資源。

  那麼怎樣禁止産生堆對象了?我們已經知道,産生堆對象的唯一方法是使用new操作,如果我們禁止使用new不就行了麼。再進一步,new操作執行時會調用operator

new,而operator new是可以重載的。方法有了,就是使new operator 為private,為了對稱,最好将operator

delete也重載為private。現在,你也許又有疑問了,難道建立棧對象不需要調用new嗎?是的,不需要,因為建立棧對象不需要搜尋記憶體,而是直接調整堆棧指針,将對象壓棧,而operator

new的主要任務是搜尋合适的堆記憶體,為堆對象配置設定空間,這在上面已經提到過了。好,讓我們看看下面的示例代碼:

#include <stdlib.h>

//需要用到c式記憶體配置設定函數

class resource ; //代表需要被封裝的資源類

nohashobject

  resource* ptr ;//指向被封裝的資源

  ... ...

//其它資料成員

  void* operator new(size_t size)

//非嚴格實作,僅作示意之用

   return malloc(size) ;

operator delete(void* pp) //非嚴格實作,僅作示意之用

   free(pp)

;

  nohashobject()

   //此處可以獲得需要封裝的資源,并讓ptr指針指向該資源

   ptr

= new resource() ;

  ~nohashobject()

   delete ptr ;

//釋放封裝的資源

  nohashobject現在就是一個禁止堆對象的類了,如果你寫下如下代碼:

nohashobject*

fp = new nohashobject() ; //編譯期錯誤!

delete fp

上面代碼會産生編譯期錯誤。好了,現在你已經知道了如何設計一個禁止堆對象的類了,你也許和我一樣有這樣的疑問,難道在類nohashobject的定義不能改變的情況下,就一定不能産生該類型的堆對象了嗎?不,還是有辦法的,我稱之為“暴力破解法”。c++是如此地強大,強大到你可以用它做你想做的任何事情。這裡主要用到的是技巧是指針類型的強制轉換。

 char* temp = new

char[sizeof(nohashobject)]

 //強制類型轉換,現在ptr是一個指向nohashobject對象的指針

 nohashobject* obj_ptr =

(nohashobject*)temp ;

 temp = null ;

//防止通過temp指針修改nohashobject對象

 //再一次強制類型轉換,讓rp指針指向堆中nohashobject對象的ptr成員

 resource*

rp = (resource*)obj_ptr ;

 //初始化obj_ptr指向的nohashobject對象的ptr成員

 rp =

new resource() ;

 //現在可以通過使用obj_ptr指針使用堆中的nohashobject對象成員了

 ...

 delete rp ;//釋放資源

 temp = (char*)obj_ptr ;

 obj_ptr = null

;//防止懸挂指針産生

 delete [] temp

;//釋放nohashobject對象所占的堆空間。

  上面的實作是麻煩的,而且這種實作方式幾乎不會在實踐中使用,但是我還是寫出來路,因為了解它,對于我們了解c++記憶體對象是有好處的。對于上面的這麼多強制類型轉換,其最根本的是什麼了?我們可以這樣了解:

  某塊記憶體中的資料是不變的,而類型就是我們戴上的眼鏡,當我們戴上一種眼鏡後,我們就會用對應的類型來解釋記憶體中的資料,這樣不同的解釋就得到了不同的資訊。

  所謂強制類型轉換實際上就是換上另一副眼鏡後再來看同樣的那塊記憶體資料。

  另外要提醒的是,不同的編譯器對對象的成員資料的布局安排可能是不一樣的,比如,大多數編譯器将nohashobject的ptr指針成員安排在對象空間的頭4個位元組,這樣才會保證下面這條語句的轉換動作像我們預期的那樣執行:

resource* rp = (resource*)obj_ptr

  但是,并不一定所有的編譯器都是如此。

  既然我們可以禁止産生某種類型的堆對象,那麼可以設計一個類,使之不能産生棧對象嗎?當然可以。

3.1.5 禁止産生棧對象

  前面已經提到了,建立棧對象時會移動棧頂指針以“挪出”适當大小的空間,然後在這個空間上直接調用對應的構造函數以形成一個棧對象,而當函數傳回時,會調用其析構函數釋放這個對象,然後再調整棧頂指針收回那塊棧記憶體。在這個過程中是不需要operator

new/delete操作的,是以将operator

new/delete設定為private不能達到目的。當然從上面的叙述中,你也許已經想到了:将構造函數或析構函數設為私有的,這樣系統就不能調用構造/析構函數了,當然就不能在棧中生成對象了。

  這樣的确可以,而且我也打算采用這種方案。但是在此之前,有一點需要考慮清楚,那就是,如果我們将構造函數設定為私有,那麼我們也就不能用new來直接産生堆對象了,因為new在為對象配置設定空間後也會調用它的構造函數啊。是以,我打算隻将析構函數設定為private。再進一步,将析構函數設為private除了會限制棧對象生成外,還有其它影響嗎?是的,這還會限制繼承。

  如果一個類不打算作為基類,通常采用的方案就是将其析構函數聲明為private。

  為了限制棧對象,卻不限制繼承,我們可以将析構函數聲明為protected,這樣就兩全其美了。如下代碼所示:

nostackobject

  ~nostackobject() {

  void destroy()

   delete this

;//調用保護析構函數

  接着,可以像這樣使用nostackobject類:

nostackobject* hash_ptr = new nostackobject()

... ... //對hash_ptr指向的對象進行操作

hash_ptr->destroy()

  呵呵,是不是覺得有點怪怪的,我們用new建立一個對象,卻不是用delete去删除它,而是要用destroy方法。很顯然,使用者是不習慣這種怪異的使用方式的。是以,我決定将構造函數也設為private或protected。這又回到了上面曾試圖避免的問題,即不用new,那麼該用什麼方式來生成一個對象了?我們可以用間接的辦法完成,即讓這個類提供一個static成員函數專門用于産生該類型的堆對象。(設計模式中的singleton模式就可以用這種方式實作。)讓我們來看看:

  nostackobject() {

  ~nostackobject() { }

  static nostackobject*

creatinstance()

   return new nostackobject()

;//調用保護的構造函數

;//調用保護的析構函數

  現在可以這樣使用nostackobject類了:

nostackobject* hash_ptr =

nostackobject::creatinstance() ;

... ...

//對hash_ptr指向的對象進行操作

hash_ptr->destroy() ;

hash_ptr = null ;

//防止使用懸挂指針 

現在感覺是不是好多了,生成對象和釋放對象的操作一緻了。

3.2 淺議c++ 中的垃圾回收方法

  許多 c 或者 c++

程式員對垃圾回收嗤之以鼻,認為垃圾回收肯定比自己來管理動态記憶體要低效,而且在回收的時候一定會讓程式停頓在那裡,而如果自己控制記憶體管理的話,配置設定和釋放時間都是穩定的,不會導緻程式停頓。最後,很多

c/c++ 程式員堅信在c/c++ 中無法實作垃圾回收機制。這些錯誤的觀點都是由于不了解垃圾回收的算法而臆想出來的。

  其實垃圾回收機制并不慢,甚至比動态記憶體配置設定更高效。因為我們可以隻配置設定不釋

放,那麼配置設定記憶體的時候隻需要從堆上一直的獲得新的記憶體,移動堆頂的指針就夠了;而釋放的過程被省略了,自然也加快了速度。現代的垃圾回收算法已經發展了

很多,增量收集算法已經可以讓垃圾回收過程分段進行,避免打斷程式的運作了。而傳統的動态記憶體管理的算法同樣有在适當的時間收集記憶體碎片的工作要做,并不

比垃圾回收更有優勢。

  而垃圾回收的算法的基礎通常基于掃描并标記目前可能被使用的所有記憶體塊,從已經被配置設定的所有記憶體中把未标記的記憶體回收來做的。c/c++ 中

無法實作垃圾回收的觀點通常基于無法正确掃描出所有可能還會被使用的記憶體塊,但是,看似不可能的事情實際上實作起來卻并不複雜。首先,通過掃描記憶體的數

據,指向堆上動态配置設定出來記憶體的指針是很容易被識别出來的,如果有識别錯誤,也隻能是把一些不是指針的資料當成指針,而不會把指針當成非指針資料。這樣,

回收垃圾的過程隻會漏回收掉而不會錯誤的把不應該回收的記憶體清理。其次,如果回溯所有記憶體塊被引用的根,隻可能存在于全局變量和目前的棧内,而全局變量(包括函數内的靜态變量)都是集中存在于

bss 段或 data段中。

  垃圾回收的時候,隻需要掃描 bss 段, data

段以及目前被使用着的棧空間,找到可能是動态記憶體指針的量,把引用到的記憶體遞歸掃描就可以得到目前正在使用的所有動态記憶體了。

  如果肯為你的工程實作一個不錯的垃圾回收器,提高記憶體管理的速度,甚至減少總的記憶體消耗都是可能的。