C语言运行时库详解
網(wǎng)址:http://blog.csdn.net/jxth152913/archive/2010/07/02/5708369.aspx
運(yùn)行時(shí)庫是程序在運(yùn)行時(shí)所需要的庫文件,通常運(yùn)行時(shí)庫是以LIB或DLL形式提供的。C運(yùn)行時(shí)庫誕生于20世紀(jì)70年代,當(dāng)時(shí)的程序世界還很單純,應(yīng)用程序都是單線程的,多任務(wù)或多線程機(jī)制在此時(shí)還屬于新觀念。所以這個(gè)時(shí)期的C運(yùn)行時(shí)庫都是單線程的。
隨著操作系統(tǒng)?多線程技術(shù)的發(fā)展?,最初的C運(yùn)行時(shí)庫無法滿足程序的需求,出現(xiàn)了嚴(yán)重的問題?。C運(yùn)行時(shí)庫使用了多個(gè)全局變量(例如errno)和靜態(tài)變量,這可能在多線程程序中引起沖突。假設(shè)兩個(gè)線程都同時(shí)設(shè)置errno,其結(jié)果是后設(shè)置的errno會(huì)將先前的覆蓋,用戶得不到正確的錯(cuò)誤信息。
因此,Visual C++提供了兩種版本的C運(yùn)行時(shí)庫。一個(gè)版本供單線程應(yīng)用程序調(diào)用,另一個(gè)版本供多線程應(yīng)用程序調(diào)用。多線程運(yùn)行時(shí)庫與單線程運(yùn)行時(shí)庫有兩個(gè)重大差別:
(1)類似errno的全局變量,每個(gè)線程單獨(dú)設(shè)置一個(gè);
這樣從每個(gè)線程中可以獲取正確的錯(cuò)誤信息。
(2)多線程庫中的數(shù)據(jù)結(jié)構(gòu)以同步機(jī)制加以保護(hù)。
這樣可以避免訪問時(shí)候的沖突。
Visual C++提供的多線程運(yùn)行時(shí)庫又分為靜態(tài)鏈接庫和動(dòng)態(tài)鏈接庫兩類,而每一類運(yùn)行時(shí)庫又可再分為debug版和release版,因此Visual C++共提供了6個(gè)運(yùn)行時(shí)庫。如下表:
C運(yùn)行時(shí)庫 庫文件
Single thread(static link) ?libc.lib
Debug single thread(static link) ?libcd.lib
MultiThread(static link) ?libcmt.lib
Debug multiThread(static link) libcmtd.lib
MultiThread(dynamic link) msvert.lib
Debug multiThread(dynamic link) msvertd.lib?
2.C運(yùn)行時(shí)庫的作用
C運(yùn)行時(shí)庫除了給我們提供必要的庫函數(shù)調(diào)用(如memcpy、printf、malloc等)之外,它提供的另一個(gè)最重要的功能是為應(yīng)用程序添加啟動(dòng)函數(shù)。
C運(yùn)行時(shí)庫啟動(dòng)函數(shù)的主要功能為進(jìn)行程序的初始化,對(duì)全局變量進(jìn)行賦初值,加載用戶程序的入口函數(shù)。
不采用寬字符集的控制臺(tái)程序的入口點(diǎn)為mainCRTStartup(void)。下面我們以該函數(shù)為例來分析運(yùn)行時(shí)庫究竟為我們添加了怎樣的入口程序。這個(gè)函數(shù)在crt0.c中被定義,下列的代碼經(jīng)過了筆者的整理和簡化:
從以上代碼可知,運(yùn)行庫在調(diào)用用戶程序的main或WinMain函數(shù)之前,進(jìn)行了一些初始化工作。初始化完成后,接著才調(diào)用了我們編寫的main或WinMain函數(shù)。只有這樣,我們的C語言運(yùn)行時(shí)庫和應(yīng)用程序才能正常地工作起來。
除了crt0.c外,C運(yùn)行時(shí)庫中還包含wcrt0.c、 wincrt0.c、wwincrt0.c三個(gè)文件用來提供初始化函數(shù)。wcrt0.c是crt0.c的寬字符集版,wincrt0.c中包含 windows應(yīng)用程序的入口函數(shù),而wwincrt0.c則是wincrt0.c的寬字符集版。
Visual C++的運(yùn)行時(shí)庫源代碼缺省情況下不被安裝。如果您想查看其源代碼,則需要重裝Visual C++,并在重裝在時(shí)選中安裝運(yùn)行庫源代碼選項(xiàng)。
3.各種C運(yùn)行時(shí)庫的區(qū)別
(1)靜態(tài)鏈接的單線程庫
靜態(tài)鏈接的單線程庫只能用于單線程的應(yīng)用程序,C運(yùn)行時(shí)庫的目標(biāo)代碼最終被編譯在應(yīng)用程序的二進(jìn)制文件中。通過/ML編譯選項(xiàng)可以設(shè)置Visual C++使用靜態(tài)鏈接的單線程庫。
(2)靜態(tài)鏈接的多線程庫
靜態(tài)鏈接的多線程庫的目標(biāo)代碼也最終被編譯在應(yīng)用程序的二進(jìn)制文件中,但是它可以在多線程程序中使用。通過/MD編譯選項(xiàng)可以設(shè)置Visual C++使用靜態(tài)鏈接的單線程庫。
(3)動(dòng)態(tài)鏈接的運(yùn)行時(shí)庫
動(dòng)態(tài)鏈接的運(yùn)行時(shí)庫將所有的C庫函數(shù)保存在一個(gè)單獨(dú)的動(dòng)態(tài)鏈接庫MSVCRTxx.DLL中,MSVCRTxx.DLL處理了多線程問題。使用/ML編譯選項(xiàng)可以設(shè)置Visual C++使用動(dòng)態(tài)鏈接的運(yùn)行時(shí)庫。
/MDd、 /MLd 或 /MTd 選項(xiàng)使用 Debug runtime library(調(diào)試版本的運(yùn)行時(shí)刻函數(shù)庫),與/MD、 /ML 或 /MT分別對(duì)應(yīng)。Debug版本的 Runtime Library 包含了調(diào)試信息,并采用了一些保護(hù)機(jī)制以幫助發(fā)現(xiàn)錯(cuò)誤,加強(qiáng)了對(duì)錯(cuò)誤的檢測(cè),因此在運(yùn)行性能方面比不上Release版本。
下面看一個(gè)未正確使用C運(yùn)行時(shí)庫的控制臺(tái)程序:
我們?cè)?#34;rebuild all"的時(shí)候發(fā)生了link錯(cuò)誤:
發(fā) 生錯(cuò)誤的原因在于Visual C++對(duì)控制臺(tái)程序默認(rèn)使用單線程的靜態(tài)鏈接庫,而MFC中的CFile類已暗藏了多線程。我們只需要在Visual C++6.0中依次點(diǎn)選Project->Settings->C/C++菜單和選項(xiàng),在Project Options里修改編譯選項(xiàng)即可。
C 運(yùn)行時(shí)庫是微軟?對(duì) 標(biāo)準(zhǔn)C庫函數(shù)的實(shí)現(xiàn),因?yàn)楫?dāng)時(shí)考慮到許多程序都使用C編寫,而這些程序都要使用標(biāo)準(zhǔn)的C庫,按照以前的方式每一個(gè)程序最終都要拷貝一份標(biāo)準(zhǔn)庫的實(shí)現(xiàn)到程序 中,這樣同一時(shí)刻內(nèi)存中可能有許多份標(biāo)準(zhǔn)庫的代碼(一個(gè)程序一份),所以微軟出于效率的考慮把 ?標(biāo)準(zhǔn)C庫做為動(dòng)態(tài)鏈接來實(shí)現(xiàn),這樣多個(gè)程序使用C標(biāo)準(zhǔn)庫時(shí)內(nèi)存中就只有一份拷貝了。(對(duì)每一個(gè)程序來說,它相當(dāng)于自己擁有一份,? ?對(duì)于標(biāo)準(zhǔn)庫中的全局變量也做了處理的,不會(huì)因?yàn)楣蚕硗环荽a而出現(xiàn)沖突)。? ?這也算是對(duì)C標(biāo)準(zhǔn)庫的一個(gè)擴(kuò)展吧,至于說靜態(tài)鏈接的時(shí)候仍然把它叫做運(yùn)行時(shí)庫那只能說這是個(gè)習(xí)慣問題而已了。??
?
?運(yùn)行時(shí)庫和普通的? ?dll? ?一樣,如果有程序用到了才會(huì)加載,沒有程序使用的時(shí)候不會(huì)駐留內(nèi)存的。話雖如此,但有多少系統(tǒng)的東西說不定也是用C寫的,這些東西的存在就使C運(yùn)行時(shí)庫存在于內(nèi)存中了
從字面上看,運(yùn)行庫是程序在運(yùn)行時(shí)所需要的庫文件。通常運(yùn)行庫是以DLL形式提供的。 Delphi和C++ Builder的運(yùn)行庫為.bpl文件,實(shí)際還是一個(gè)DLL。運(yùn)行庫中一般包括編程時(shí)常用的函數(shù),如字符串操作、文件操作、界面等內(nèi)容。不同的語言所支持 的函數(shù)通常是不同的,所以使用的庫也是完全不同的,這就是為什么有VB運(yùn)行庫、C運(yùn)行庫、Delphi運(yùn)行庫之分的原因。即使都是C++語言,也可能因?yàn)?提供的函數(shù)不同,而使用不同的庫。如VC++使用的運(yùn)行庫和C++ Builder就完全不同。
如果不使用運(yùn)行庫,每個(gè)程序中都會(huì)包括很多重復(fù)的代碼,而使用運(yùn)行庫,可以大大縮小編譯后的程序的大小。但另一方面,由于使用了運(yùn)行庫,所以在分發(fā)程序時(shí)就必須帶有這些庫,比較麻煩。如果在操作系統(tǒng)中找不到相應(yīng)的運(yùn)行庫程序就無法運(yùn)行。為了解決這個(gè)矛盾,Windows?總 是會(huì)帶上它自己開發(fā)的軟件的最新的運(yùn)行庫。象Windows 2000以后的版本都包括Visual Basic 5.0/6.0的庫。Internet Explorer總是帶有最新的Visual C++ 6.0的庫。Windows XP帶有Microsoft .NET 1.0(用于VB.NET和C#)的庫。Visual C++、Delphi和C++ Builder允許用戶選擇所編譯得到的程序是否依賴于運(yùn)行庫。而VB、FoxPro、PowerBuilder、LabWindows/CVI和 Matlab就不允許用戶進(jìn)行這種選擇,必須依賴于運(yùn)行庫。
2 內(nèi)存泄漏
2.1 C++中動(dòng)態(tài)內(nèi)存分配引發(fā)問題的解決方案
假設(shè)我們要開發(fā)一個(gè)String類,它可以方便地處理字符串?dāng)?shù)據(jù)。我們可以在類中聲明一個(gè)數(shù)組,考慮到有時(shí)候字符串極長,我們可以把數(shù)組大小設(shè)為200, 但一般的情況下又不需要這么多的空間,這樣是浪費(fèi)了內(nèi)存。對(duì)了,我們可以使用new操作符,這樣是十分靈活的,但在類中就會(huì)出現(xiàn)許多意想不到的問題,本文 就是針對(duì)這一現(xiàn)象而寫的。現(xiàn)在,我們先來開發(fā)一個(gè)String類,但它是一個(gè)不完善的類。的確,我們要刻意地使它出現(xiàn)各種各樣的問題,這樣才好對(duì)癥下藥。 好了,我們開始吧!
運(yùn)行結(jié)果:
大家網(wǎng)
請(qǐng)按任意鍵繼續(xù). . .
大家可以看到,以上程序十分正確,而且也是十分有用的。可是,我們不能被表面現(xiàn)象所迷惑!下面,請(qǐng)大家用test_String.cpp文件替換test_right.cpp文件進(jìn)行編譯,看看結(jié)果。有的編譯器可能就是根本不能進(jìn)行編譯!
運(yùn)行結(jié)果:
下面分別輸入三個(gè)范例:
第一個(gè)范例。
第二個(gè)范例。
第三個(gè)范例。
第一個(gè)范例。
這個(gè)字符串將被刪除:第一個(gè)范例。
使用正確的函數(shù):
第二個(gè)范例。
第二個(gè)范例。
使用錯(cuò)誤的函數(shù):
第二個(gè)范例。
這個(gè)字符串將被刪除:第二個(gè)范例。
這個(gè)字符串將被刪除:?=
?=
String2: 第三個(gè)范例。
String3: 第四個(gè)范例。
下面,程序結(jié)束,析構(gòu)函數(shù)將被調(diào)用。
這個(gè)字符串將被刪除:第四個(gè)范例。
這個(gè)字符串將被刪除:第三個(gè)范例。
這個(gè)字符串將被刪除:?=
這個(gè)字符串將被刪除:x =
這個(gè)字符串將被刪除:?=
這個(gè)字符串將被刪除:
現(xiàn)在,請(qǐng)大家自己試試運(yùn)行結(jié)果,或許會(huì)更加慘不忍睹呢!下面,我為大家一一分析原因。
首先,大家要知道,C++類有以下這些極為重要的函數(shù):
一:復(fù)制構(gòu)造函數(shù)。
二:賦值函數(shù)。
我們先來講復(fù)制構(gòu)造函數(shù)。什么是復(fù)制構(gòu)造函數(shù)呢?比如,我們可以寫下這樣的代碼:String test1(test2);這是進(jìn)行初始化。我們知道,初始化對(duì)象要用構(gòu)造函數(shù)。可這兒呢?按理說,應(yīng)該有聲明為這樣的構(gòu)造函 數(shù):String(const String &);可是,我們并沒有定義這個(gè)構(gòu)造函數(shù)呀?答案是,C++提供了默認(rèn)的復(fù)制構(gòu)造函數(shù),問題也就出在這兒。
(1):什么時(shí)候會(huì)調(diào)用復(fù)制構(gòu)造函數(shù)呢?(以String類為例。)
在我們提供這樣的代碼:String test1(test2)時(shí),它會(huì)被調(diào)用;當(dāng)函數(shù)的參數(shù)列表為按值傳遞,也就是沒有用引用和指針作為類型時(shí),如:void show_String(const String),它會(huì)被調(diào)用。其實(shí),還有一些情況,但在這兒就不列舉了。
(2):它是什么樣的函數(shù)。
它的作用就是把兩個(gè)類進(jìn)行復(fù)制。拿String類為例,C++提供的默認(rèn)復(fù)制構(gòu)造函數(shù)是這樣的:
在 平時(shí),這樣并不會(huì)有任何的問題出現(xiàn),但我們用了new操作符,涉及到了動(dòng)態(tài)內(nèi)存分配,我們就不得不談?wù)劀\復(fù)制和深復(fù)制了。以上的函數(shù)就是實(shí)行的淺復(fù)制,它 只是復(fù)制了指針,而并沒有復(fù)制指針指向的數(shù)據(jù),可謂一點(diǎn)兒用也沒有。打個(gè)比方吧!就像一個(gè)朋友讓你把一個(gè)程序通過網(wǎng)絡(luò)發(fā)給他,而你大大咧咧地把快捷方式發(fā) 給了他,有什么用處呢?我們來具體談?wù)?#xff1a;
假如,A對(duì)象中存儲(chǔ)了這樣的字符串:“C++”。它的地址為2000。現(xiàn)在,我們把A對(duì)象賦給B對(duì)象:String B=A。現(xiàn)在,A和B對(duì)象的str指針均指向2000地址。看似可以使用,但如果B對(duì)象的析構(gòu)函數(shù)被調(diào)用時(shí),則地址2000處的字符串“C++”已經(jīng)被從 內(nèi)存中抹去,而A對(duì)象仍然指向地址2000。這時(shí),如果我們寫下這樣的代碼:cout<<A<<endl;或是等待程序結(jié)束,A 對(duì)象的析構(gòu)函數(shù)被調(diào)用時(shí),A對(duì)象的數(shù)據(jù)能否顯示出來呢?只會(huì)是亂碼。而且,程序還會(huì)這樣做:連續(xù)對(duì)地址2000處使用兩次delete操作符,這樣的后果 是十分嚴(yán)重的!
本例中,有這樣的代碼:
假 設(shè)test1中str指向的地址為2000,而String中str指針同樣指向地址2000,我們刪除了2000處的數(shù)據(jù),而test1對(duì)象呢?已經(jīng)被 破壞了。大家從運(yùn)行結(jié)果上可以看到,我們使用cout<<test1時(shí),一點(diǎn)反應(yīng)也沒有。而在test1的析構(gòu)函數(shù)被調(diào)用時(shí),顯示是這樣: “這個(gè)字符串將被刪除:”。
再看看這段代碼:
show_String 函數(shù)的參數(shù)列表void show_String(const String a)是按值傳遞的,所以,我們相當(dāng)于執(zhí)行了這樣的代碼:String a=test2;函數(shù)執(zhí)行完畢,由于生存周期的緣故,對(duì)象a被析構(gòu)函數(shù)刪除,我們馬上就可以看到錯(cuò)誤的顯示結(jié)果了:這個(gè)字符串將被刪除:?=。當(dāng) 然,test2也被破壞了。解決的辦法很簡單,當(dāng)然是手工定義一個(gè)復(fù)制構(gòu)造函數(shù)嘍!人力可以勝天!
我 們執(zhí)行的是深復(fù)制。這個(gè)函數(shù)的功能是這樣的:假設(shè)對(duì)象A中的str指針指向地址2000,內(nèi)容為“I am a C++ Boy!”。我們執(zhí)行代碼String B=A時(shí),我們先開辟出一塊內(nèi)存,假設(shè)為3000。我們用strcpy函數(shù)將地址2000的內(nèi)容拷貝到地址3000中,再將對(duì)象B的str指針指向地址 3000。這樣,就互不干擾了。
大家把這個(gè)函數(shù)加入程序中,問題就解決了大半,但還沒有完全解決,問題在賦值函數(shù)上。我們的程序中有這樣的段代碼:
經(jīng) 過我前面的講解,大家應(yīng)該也會(huì)對(duì)這段代碼進(jìn)行尋根摸底:憑什么可以這樣做:String3=test4???原因是,C++為了用戶的方便,提供的這樣的 一個(gè)操作符重載函數(shù):operator=。所以,我們可以這樣做。大家應(yīng)該猜得到,它同樣是執(zhí)行了淺復(fù)制,出了同樣的毛病。比如,執(zhí)行了這段代碼后,析構(gòu) 函數(shù)開始大展神威^_^。由于這些變量是后進(jìn)先出的,所以最后的String3變量先被刪除:這個(gè)字符串將被刪除:第四個(gè)范例。很正常。最后,刪除到 test4的時(shí)候,問題來了:這個(gè)字符串將被刪除:?=。原因我不用贅述了,只是這個(gè)賦值函數(shù)怎么寫,還有一點(diǎn)兒學(xué)問呢!大家請(qǐng)看:
平時(shí),我們可以寫這樣的代碼:x=y=z。(均為整型變量。)而在類對(duì)象中,我們同樣要這樣,因?yàn)檫@很方便。而對(duì)象A=B=C就是 A.operator=(B.operator=(c))。而這個(gè)operator=函數(shù)的參數(shù)列表應(yīng)該是:const String& a,所以,大家不難推出,要實(shí)現(xiàn)這樣的功能,返回值也要是String&,這樣才能實(shí)現(xiàn)A=B=C。我們先來寫寫看:
是 不是這樣就行了呢?我們假如寫出了這種代碼:A=A,那么大家看看,豈不是把A對(duì)象的數(shù)據(jù)給刪除了嗎?這樣可謂引發(fā)一系列的錯(cuò)誤。所以,我們還要檢查是否 為自身賦值。只比較兩對(duì)象的數(shù)據(jù)是不行了,因?yàn)閮蓚€(gè)對(duì)象的數(shù)據(jù)很有可能相同。我們應(yīng)該比較地址。以下是完好的賦值函數(shù):
把這些代碼加入程序,問題就完全解決,下面是運(yùn)行結(jié)果:
下面分別輸入三個(gè)范例:
第一個(gè)范例
第二個(gè)范例
第三個(gè)范例
第一個(gè)范例
這個(gè)字符串將被刪除:第一個(gè)范例。
第一個(gè)范例
使用正確的函數(shù):
第二個(gè)范例。
第二個(gè)范例。
使用錯(cuò)誤的函數(shù):
第二個(gè)范例。
這個(gè)字符串將被刪除:第二個(gè)范例。
第二個(gè)范例。
String2: 第三個(gè)范例。
String3: 第四個(gè)范例。
下面,程序結(jié)束,析構(gòu)函數(shù)將被調(diào)用。
這個(gè)字符串將被刪除:第四個(gè)范例。
這個(gè)字符串將被刪除:第三個(gè)范例。
這個(gè)字符串將被刪除:第四個(gè)范例。
這個(gè)字符串將被刪除:第三個(gè)范例。
這個(gè)字符串將被刪除:第二個(gè)范例。
這個(gè)字符串將被刪除:第一個(gè)范例。
2.2 如何對(duì)付內(nèi)存泄漏?
寫出那些不會(huì)導(dǎo)致任何內(nèi)存泄漏的代碼。很明顯,當(dāng)你的代碼中到處充滿了new 操作、delete操作和指針運(yùn)算的話,你將會(huì)在某個(gè)地方搞暈了頭,導(dǎo)致內(nèi)存泄漏,指針引用錯(cuò)誤,以及諸如此類的問題。這和你如何小心地對(duì)待內(nèi)存分配工作 其實(shí)完全沒有關(guān)系:代碼的復(fù)雜性最終總是會(huì)超過你能夠付出的時(shí)間和努力。于是隨后產(chǎn)生了一些成功的技巧,它們依賴于將內(nèi)存分配(allocations) 與重新分配(deallocation)工作隱藏在易于管理的類型之后。標(biāo)準(zhǔn)容器(standard containers)是一個(gè)優(yōu)秀的例子。它們不是通過你而是自己為元素管理內(nèi)存,從而避免了產(chǎn)生糟糕的結(jié)果。想象一下,沒有string和vector 的幫助,寫出這個(gè):
你有多少機(jī)會(huì)在第一次就得到正確的結(jié)果?你又怎么知道你沒有導(dǎo)致內(nèi)存泄漏呢?
注意,沒有出現(xiàn)顯式的內(nèi)存管理,宏,造型,溢出檢查,顯式的長度限制,以及指針。通過使用函數(shù)對(duì)象和標(biāo)準(zhǔn)算法(standard algorithm),我可以避免使用指針——例如使用迭代子(iterator),不過對(duì)于一個(gè)這么小的程序來說有點(diǎn)小題大作了。
這些技巧并不完美,要系統(tǒng)化地使用它們也并不總是那么容易。但是,應(yīng)用它們產(chǎn)生了驚人的差異,而且通過減少顯式的內(nèi)存分配與重新分配的次數(shù),你甚至可 以使余下的例子更加容易被跟蹤。早在1981年,我就指出,通過將我必須顯式地跟蹤的對(duì)象的數(shù)量從幾萬個(gè)減少到幾打,為了使程序正確運(yùn)行而付出的努力從可 怕的苦工,變成了應(yīng)付一些可管理的對(duì)象,甚至更加簡單了。
如果你的程序還沒有包含將顯式內(nèi)存管理減少到最小限度的庫,那么要讓你程序完成和正確運(yùn)行的話,最快的途徑也許就是先建立一個(gè)這樣的庫。
模板和標(biāo)準(zhǔn)庫實(shí)現(xiàn)了容器、資源句柄以及諸如此類的東西,更早的使用甚至在多年以前。異常的使用使之更加完善。
如果你實(shí)在不能將內(nèi)存分配/重新分配的操作隱藏到你需要的對(duì)象中時(shí),你可以使用資源句柄(resource handle),以將內(nèi)存泄漏的可能性降至最低。這里有個(gè)例子:我需要通過一個(gè)函數(shù),在空閑內(nèi)存中建立一個(gè)對(duì)象并返回它。這時(shí)候可能忘記釋放這個(gè)對(duì)象。畢 竟,我們不能說,僅僅關(guān)注當(dāng)這個(gè)指針要被釋放的時(shí)候,誰將負(fù)責(zé)去做。使用資源句柄,這里用了標(biāo)準(zhǔn)庫中的auto_ptr,使需要為之負(fù)責(zé)的地方變得明確 了。
在更一般的意義上考慮資源,而不僅僅是內(nèi)存。
如果在你的環(huán)境中不能系統(tǒng)地應(yīng)用這些技巧(例如,你必須使用別的地方的代碼,或者你的程序的另一部分簡直是原始人類(譯注:原文是 Neanderthals,尼安德特人,舊石器時(shí)代廣泛分布在歐洲的猿人)寫的,如此等等),那么注意使用一個(gè)內(nèi)存泄漏檢測(cè)器作為開發(fā)過程的一部分,或者 插入一個(gè)垃圾收集器(garbage collector)。
2.3淺談C/C++內(nèi)存泄漏及其檢測(cè)工具
對(duì)于一個(gè)c/c++程序員來說,內(nèi)存泄漏是一個(gè)常見的也是令人頭疼的問題。已經(jīng)有許多技術(shù)被研究出來以應(yīng)對(duì)這個(gè)問題,比如Smart Pointer,Garbage Collection等。Smart Pointer技術(shù)比較成熟,STL中已經(jīng)包含支持Smart Pointer的class,但是它的使用似乎并不廣泛,而且它也不能解決所有的問題;Garbage Collection技術(shù)在Java中已經(jīng)比較成熟,但是在c/c++領(lǐng)域的發(fā)展并不順暢,雖然很早就有人思考在C++中也加入GC的支持。現(xiàn)實(shí)世界就是 這樣的,作為一個(gè)c/c++程序員,內(nèi)存泄漏是你心中永遠(yuǎn)的痛。不過好在現(xiàn)在有許多工具能夠幫助我們驗(yàn)證內(nèi)存泄漏的存在,找出發(fā)生問題的代碼。
2.3.1 內(nèi)存泄漏的定義
一般我們常說的內(nèi)存泄漏是指堆內(nèi)存的泄漏。堆內(nèi)存是指程序從堆中分配的,大小任意的(內(nèi)存塊的大小可以在程序運(yùn)行期決定),使用完后必須顯示釋放的內(nèi)存。 應(yīng)用程序一般使用malloc,realloc,new等函數(shù)從堆中分配到一塊內(nèi)存,使用完后,程序必須負(fù)責(zé)相應(yīng)的調(diào)用free或delete釋放該內(nèi)存 塊,否則,這塊內(nèi)存就不能被再次使用,我們就說這塊內(nèi)存泄漏了。以下這段小程序演示了堆內(nèi)存發(fā)生泄漏的情形:
當(dāng)函數(shù)GetStringFrom()返回零的時(shí)候,指針p指向的內(nèi)存就不會(huì)被釋放。這是一種常見的發(fā)生內(nèi)存泄漏的情形。程序在入口處分配內(nèi)存,在出口處釋放內(nèi)存,但是c函數(shù)可以在任何地方退出,所以一旦有某個(gè)出口處沒有釋放應(yīng)該釋放的內(nèi)存,就會(huì)發(fā)生內(nèi)存泄漏。
廣義的說,內(nèi)存泄漏不僅僅包含堆內(nèi)存的泄漏,還包含系統(tǒng)資源的泄漏(resource leak),比如核心態(tài)HANDLE,GDI Object,SOCKET, Interface等,從根本上說這些由操作系統(tǒng)分配的對(duì)象也消耗內(nèi)存,如果這些對(duì)象發(fā)生泄漏最終也會(huì)導(dǎo)致內(nèi)存的泄漏。而且,某些對(duì)象消耗的是核心態(tài)內(nèi) 存,這些對(duì)象嚴(yán)重泄漏時(shí)會(huì)導(dǎo)致整個(gè)操作系統(tǒng)不穩(wěn)定。所以相比之下,系統(tǒng)資源的泄漏比堆內(nèi)存的泄漏更為嚴(yán)重。
GDI Object的泄漏是一種常見的資源泄漏:
當(dāng) 函數(shù)Something()返回非零的時(shí)候,程序在退出前沒有把pOldBmp選回pDC中,這會(huì)導(dǎo)致pOldBmp指向的HBITMAP對(duì)象發(fā)生泄漏。 這個(gè)程序如果長時(shí)間的運(yùn)行,可能會(huì)導(dǎo)致整個(gè)系統(tǒng)花屏。這種問題在Win9x下比較容易暴露出來,因?yàn)閃in9x的GDI堆比Win2k或NT 的要小很多。
2.3.2 內(nèi)存泄漏的發(fā)生方式
以發(fā)生的方式來分類,內(nèi)存泄漏可以分為4類:
1. 常發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼會(huì)被多次執(zhí)行到,每次被執(zhí)行的時(shí)候都會(huì)導(dǎo)致一塊內(nèi)存泄漏。比如例二,如果Something()函數(shù)一直返回True,那么pOldBmp指向的HBITMAP對(duì)象總是發(fā)生泄漏。
2. 偶發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只有在某些特定環(huán)境或操作過程下才會(huì)發(fā)生。比如例二,如果Something()函數(shù)只有在特定環(huán)境下才返回 True,那么pOldBmp指向的HBITMAP對(duì)象并不總是發(fā)生泄漏。常發(fā)性和偶發(fā)性是相對(duì)的。對(duì)于特定的環(huán)境,偶發(fā)性的也許就變成了常發(fā)性的。所以 測(cè)試環(huán)境和測(cè)試方法對(duì)檢測(cè)內(nèi)存泄漏至關(guān)重要。
3. 一次性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只會(huì)被執(zhí)行一次,或者由于算法上的缺陷,導(dǎo)致總會(huì)有一塊僅且一塊內(nèi)存發(fā)生泄漏。比如,在類的構(gòu)造函數(shù)中分配內(nèi)存,在析構(gòu)函數(shù)中卻沒有釋放該內(nèi)存,但是因?yàn)檫@個(gè)類是一個(gè)Singleton,所以內(nèi)存泄漏只會(huì)發(fā)生一次。另一個(gè)例子:
如果程序在結(jié)束的時(shí)候沒有釋放g_lpszFileName指向的字符串,那么,即使多次調(diào)用SetFileName(),總會(huì)有一塊內(nèi)存,而且僅有一塊內(nèi)存發(fā)生泄漏。
4. 隱式內(nèi)存泄漏。程序在運(yùn)行過程中不停的分配內(nèi)存,但是直到結(jié)束的時(shí)候才釋放內(nèi)存。嚴(yán)格的說這里并沒有發(fā)生內(nèi)存泄漏,因?yàn)樽罱K程序釋放了所有申請(qǐng)的內(nèi)存。但 是對(duì)于一個(gè)服務(wù)器程序,需要運(yùn)行幾天,幾周甚至幾個(gè)月,不及時(shí)釋放內(nèi)存也可能導(dǎo)致最終耗盡系統(tǒng)的所有內(nèi)存。所以,我們稱這類內(nèi)存泄漏為隱式內(nèi)存泄漏。舉一 個(gè)例子:
假 設(shè)在Client從Server端斷開后,Server并沒有呼叫OnClientDisconnected()函數(shù),那么代表那次連接的 Connection對(duì)象就不會(huì)被及時(shí)的刪除(在Server程序退出的時(shí)候,所有Connection對(duì)象會(huì)在ConnectionManager的析 構(gòu)函數(shù)里被刪除)。當(dāng)不斷的有連接建立、斷開時(shí)隱式內(nèi)存泄漏就發(fā)生了。
從用戶使用程序的角度來看,內(nèi)存泄漏本身不會(huì)產(chǎn)生什么危害,作為一般的用戶,根本感覺不到內(nèi)存泄漏的存在。真正有危害的是內(nèi)存泄漏的堆積,這會(huì)最終消耗盡 系統(tǒng)所有的內(nèi)存。從這個(gè)角度來說,一次性內(nèi)存泄漏并沒有什么危害,因?yàn)樗粫?huì)堆積,而隱式內(nèi)存泄漏危害性則非常大,因?yàn)檩^之于常發(fā)性和偶發(fā)性內(nèi)存泄漏它更 難被檢測(cè)到。
2.3.3 檢測(cè)內(nèi)存泄漏
檢測(cè)內(nèi)存泄漏的關(guān)鍵是要能截獲住對(duì)分配內(nèi)存和釋放內(nèi)存的函數(shù)的調(diào)用。截獲住這兩個(gè)函數(shù),我們就能跟蹤每一塊內(nèi)存的生命周期,比如,每當(dāng)成功的分配一塊 內(nèi)存后,就把它的指針加入一個(gè)全局的list中;每當(dāng)釋放一塊內(nèi)存,再把它的指針從list中刪除。這樣,當(dāng)程序結(jié)束的時(shí)候,list中剩余的指針就是指 向那些沒有被釋放的內(nèi)存。這里只是簡單的描述了檢測(cè)內(nèi)存泄漏的基本原理,詳細(xì)的算法可以參見Steve Maguire的<<Writing Solid Code>>。
如果要檢測(cè)堆內(nèi)存的泄漏,那么需要截獲住malloc/realloc/free和new/delete就可以了(其實(shí)new/delete最終也是 用malloc/free的,所以只要截獲前面一組即可)。對(duì)于其他的泄漏,可以采用類似的方法,截獲住相應(yīng)的分配和釋放函數(shù)。比如,要檢測(cè) BSTR的泄漏,就需要截獲SysAllocString/SysFreeString;要檢測(cè)HMENU的泄漏,就需要截獲CreateMenu/ DestroyMenu。(有的資源的分配函數(shù)有多個(gè),釋放函數(shù)只有一個(gè),比如,SysAllocStringLen也可以用來分配BSTR,這時(shí)就需要 截獲多個(gè)分配函數(shù))
在Windows平臺(tái)下,檢測(cè)內(nèi)存泄漏的工具常用的一般有三種,MS C-Runtime Library內(nèi)建的檢測(cè)功能;外掛式的檢測(cè)工具,諸如,Purify,BoundsChecker等;利用Windows NT自帶的Performance Monitor。這三種工具各有優(yōu)缺點(diǎn),MS C-Runtime Library雖然功能上較之外掛式的工具要弱,但是它是免費(fèi)的;Performance Monitor雖然無法標(biāo)示出發(fā)生問題的代碼,但是它能檢測(cè)出隱式的內(nèi)存泄漏的存在,這是其他兩類工具無能為力的地方。
以下我們?cè)敿?xì)討論這三種檢測(cè)工具:
2.3.3.1 VC下內(nèi)存泄漏的檢測(cè)方法
用MFC開發(fā)的應(yīng)用程序,在DEBUG版模式下編譯后,都會(huì)自動(dòng)加入內(nèi)存泄漏的檢測(cè)代碼。在程序結(jié)束后,如果發(fā)生了內(nèi)存泄漏,在Debug窗口中會(huì)顯示出所有發(fā)生泄漏的內(nèi)存塊的信息,以下兩行顯示了一塊被泄漏的內(nèi)存塊的信息:
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
第一行顯示該內(nèi)存塊由TestDlg.cpp文件,第70行代碼分配,地址在0x00881710,大小為200字節(jié),{59}是指調(diào)用內(nèi)存分配函數(shù) 的Request Order,關(guān)于它的詳細(xì)信息可以參見MSDN中_CrtSetBreakAlloc()的幫助。第二行顯示該內(nèi)存塊前16個(gè)字節(jié)的內(nèi)容,尖括號(hào)內(nèi)是以 ASCII方式顯示,接著的是以16進(jìn)制方式顯示。
一般大家都誤以為這些內(nèi)存泄漏的檢測(cè)功能是由MFC提供的,其實(shí)不然。MFC只是封裝和利用了MS C-Runtime Library的Debug Function。非MFC程序也可以利用MS C-Runtime Library的Debug Function加入內(nèi)存泄漏的檢測(cè)功能。MS C-Runtime Library在實(shí)現(xiàn)malloc/free,strdup等函數(shù)時(shí)已經(jīng)內(nèi)建了內(nèi)存泄漏的檢測(cè)功能。
注意觀察一下由MFC Application Wizard生成的項(xiàng)目,在每一個(gè)cpp文件的頭部都有這樣一段宏定義:
有了這樣的定義,在編譯DEBUG版時(shí),出現(xiàn)在這個(gè)cpp文件中的所有new都被替換成DEBUG_NEW了。那么DEBUG_NEW是什么呢?DEBUG_NEW也是一個(gè)宏,以下摘自afx.h,1632行
所以如果有這樣一行代碼:
經(jīng)過宏替換就變成了:
根據(jù)C++的標(biāo)準(zhǔn),對(duì)于以上的new的使用方法,編譯器會(huì)去找這樣定義的operator new:
我們?cè)赼fxmem.cpp 63行找到了一個(gè)這樣的operator new 的實(shí)現(xiàn)
第 二個(gè)operator new函數(shù)比較長,為了簡單期間,我只摘錄了部分。很顯然最后的內(nèi)存分配還是通過_malloc_dbg函數(shù)實(shí)現(xiàn)的,這個(gè)函數(shù)屬于MS C-Runtime Library 的Debug Function。這個(gè)函數(shù)不但要求傳入內(nèi)存的大小,另外還有文件名和行號(hào)兩個(gè)參數(shù)。文件名和行號(hào)就是用來記錄此次分配是由哪一段代碼造成的。如果這塊內(nèi) 存在程序結(jié)束之前沒有被釋放,那么這些信息就會(huì)輸出到Debug窗口里。
這里順便提一下THIS_FILE,__FILE和__LINE__。__FILE__和__LINE__都是編譯器定義的宏。當(dāng)碰到 __FILE__時(shí),編譯器會(huì)把__FILE__替換成一個(gè)字符串,這個(gè)字符串就是當(dāng)前在編譯的文件的路徑名。當(dāng)碰到__LINE__時(shí),編譯器會(huì)把 __LINE__替換成一個(gè)數(shù)字,這個(gè)數(shù)字就是當(dāng)前這行代碼的行號(hào)。在DEBUG_NEW的定義中沒有直接使用__FILE__,而是用了 THIS_FILE,其目的是為了減小目標(biāo)文件的大小。假設(shè)在某個(gè)cpp文件中有100處使用了new,如果直接使用__FILE__,那編譯器會(huì)產(chǎn)生 100個(gè)常量字符串,這100個(gè)字符串都是飧?/SPAN>cpp文件的路徑名,顯然十分冗余。如果使用THIS_FILE,編譯器只會(huì)產(chǎn)生一個(gè)常 量字符串,那100處new的調(diào)用使用的都是指向常量字符串的指針。
再次觀察一下由MFC Application Wizard生成的項(xiàng)目,我們會(huì)發(fā)現(xiàn)在cpp文件中只對(duì)new做了映射,如果你在程序中直接使用malloc函數(shù)分配內(nèi)存,調(diào)用malloc的文件名和行 號(hào)是不會(huì)被記錄下來的。如果這塊內(nèi)存發(fā)生了泄漏,MS C-Runtime Library仍然能檢測(cè)到,但是當(dāng)輸出這塊內(nèi)存塊的信息,不會(huì)包含分配它的的文件名和行號(hào)。
要在非MFC程序中打開內(nèi)存泄漏的檢測(cè)功能非常容易,你只要在程序的入口處加入以下幾行代碼:
這樣,在程序結(jié)束的時(shí)候,也就是winmain,main或dllmain函數(shù)返回之后,如果還有內(nèi)存塊沒有釋放,它們的信息會(huì)被打印到Debug窗口里。
如果你試著創(chuàng)建了一個(gè)非MFC應(yīng)用程序,而且在程序的入口處加入了以上代碼,并且故意在程序中不釋放某些內(nèi)存塊,你會(huì)在Debug窗口里看到以下的信息:
{47} normal block at 0x00C91C90, 200 bytes long.
Data: < > 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
內(nèi)存泄漏的確檢測(cè)到了,但是和上面MFC程序的例子相比,缺少了文件名和行號(hào)。對(duì)于一個(gè)比較大的程序,沒有這些信息,解決問題將變得十分困難。
為了能夠知道泄漏的內(nèi)存塊是在哪里分配的,你需要實(shí)現(xiàn)類似MFC的映射功能,把new,maolloc等函數(shù)映射到_malloc_dbg函數(shù)上。這里我不再贅述,你可以參考MFC的源代碼。
由于Debug Function實(shí)現(xiàn)在MS C-RuntimeLibrary中,所以它只能檢測(cè)到堆內(nèi)存的泄漏,而且只限于malloc,realloc或strdup等分配的內(nèi)存,而那些系統(tǒng)資 源,比如HANDLE,GDI Object,或是不通過C-Runtime Library分配的內(nèi)存,比如VARIANT,BSTR的泄漏,它是無法檢測(cè)到的,這是這種檢測(cè)法的一個(gè)重大的局限性。另外,為了能記錄內(nèi)存塊是在哪里 分配的,源代碼必須相應(yīng)的配合,這在調(diào)試一些老的程序非常麻煩,畢竟修改源代碼不是一件省心的事,這是這種檢測(cè)法的另一個(gè)局限性。
對(duì)于開發(fā)一個(gè)大型的程序,MS C-Runtime Library提供的檢測(cè)功能是遠(yuǎn)遠(yuǎn)不夠的。接下來我們就看看外掛式的檢測(cè)工具。我用的比較多的是BoundsChecker,一則因?yàn)樗墓δ鼙容^全 面,更重要的是它的穩(wěn)定性。這類工具如果不穩(wěn)定,反而會(huì)忙里添亂。到底是出自鼎鼎大名的NuMega,我用下來基本上沒有什么大問題。
2.3.3.2 使用BoundsChecker檢測(cè)內(nèi)存泄漏
BoundsChecker采用一種被稱為 Code Injection的技術(shù),來截獲對(duì)分配內(nèi)存和釋放內(nèi)存的函數(shù)的調(diào)用。簡單地說,當(dāng)你的程序開始運(yùn)行時(shí),BoundsChecker的DLL被自動(dòng)載入進(jìn) 程的地址空間(這可以通過system-level的Hook實(shí)現(xiàn)),然后它會(huì)修改進(jìn)程中對(duì)內(nèi)存分配和釋放的函數(shù)調(diào)用,讓這些調(diào)用首先轉(zhuǎn)入它的代碼,然后 再執(zhí)行原來的代碼。BoundsChecker在做這些動(dòng)作的時(shí),無須修改被調(diào)試程序的源代碼或工程配置文件,這使得使用它非常的簡便、直接。
這里我們以malloc函數(shù)為例,截獲其他的函數(shù)方法與此類似。
需要被截獲的函數(shù)可能在DLL中,也可能在程序的代碼里。比如,如果靜態(tài)連結(jié)C-Runtime Library,那么malloc函數(shù)的代碼會(huì)被連結(jié)到程序里。為了截獲住對(duì)這類函數(shù)的調(diào)用,BoundsChecker會(huì)動(dòng)態(tài)修改這些函數(shù)的指令。
以下兩段匯編代碼,一段沒有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介入:
126: _CRTIMP void * __cdecl malloc (
127: size_t nSize
128: )
129: {
00403C10 jmp 01F41EC8
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: }
當(dāng)BoundsChecker介入后,函數(shù)malloc的前三條匯編指令被替換成一條jmp指令,原來的三條指令被搬到地址01F41EC8處了。當(dāng) 程序進(jìn)入malloc后先jmp到01F41EC8,執(zhí)行原來的三條指令,然后就是BoundsChecker的天下了。大致上它會(huì)先記錄函數(shù)的返回地址 (函數(shù)的返回地址在stack上,所以很容易修改),然后把返回地址指向?qū)儆贐oundsChecker的代碼,接著跳到malloc函數(shù)原來的指令,也 就是在00403c15的地方。當(dāng)malloc函數(shù)結(jié)束的時(shí)候,由于返回地址被修改,它會(huì)返回到BoundsChecker的代碼中,此時(shí) BoundsChecker會(huì)記錄由malloc分配的內(nèi)存的指針,然后再跳轉(zhuǎn)到到原來的返回地址去。
如果內(nèi)存分配/釋放函數(shù)在DLL中,BoundsChecker則采用另一種方法來截獲對(duì)這些函數(shù)的調(diào)用。BoundsChecker通過修改程序的DLL Import Table讓table中的函數(shù)地址指向自己的地址,以達(dá)到截獲的目的。
截獲住這些分配和釋放函數(shù),BoundsChecker就能記錄被分配的內(nèi)存或資源的生命周期。接下來的問題是如何與源代碼相關(guān),也就是說當(dāng) BoundsChecker檢測(cè)到內(nèi)存泄漏,它如何報(bào)告這塊內(nèi)存塊是哪段代碼分配的。答案是調(diào)試信息(Debug Information)。當(dāng)我們編譯一個(gè)Debug版的程序時(shí),編譯器會(huì)把源代碼和二進(jìn)制代碼之間的對(duì)應(yīng)關(guān)系記錄下來,放到一個(gè)單獨(dú)的文件里 (.pdb)或者直接連結(jié)進(jìn)目標(biāo)程序,通過直接讀取調(diào)試信息就能得到分配某塊內(nèi)存的源代碼在哪個(gè)文件,哪一行上。使用Code Injection和Debug Information,使BoundsChecker不但能記錄呼叫分配函數(shù)的源代碼的位置,而且還能記錄分配時(shí)的Call Stack,以及Call Stack上的函數(shù)的源代碼位置。這在使用像MFC這樣的類庫時(shí)非常有用,以下我用一個(gè)例子來說明:
void ShowXItemMenu()
{
…
CMenu menu;
menu.CreatePopupMenu();
//add menu items.
menu.TrackPropupMenu();
…
}
void ShowYItemMenu( )
{
…
CMenu menu;
menu.CreatePopupMenu();
//add menu items.
menu.TrackPropupMenu();
menu.Detach();//this will cause HMENU leak
…
}
BOOL CMenu::CreatePopupMenu()
{
…
hMenu = CreatePopupMenu();
…
}
當(dāng)調(diào)用ShowYItemMenu()時(shí),我們故意造成HMENU的泄漏。但是,對(duì)于BoundsChecker來說被泄漏的HMENU是在 class CMenu::CreatePopupMenu()中分配的。假設(shè)的你的程序有許多地方使用了CMenu的CreatePopupMenu()函數(shù),如 CMenu::CreatePopupMenu()造成的,你依然無法確認(rèn)問題的根結(jié)到底在哪里,在ShowXItemMenu()中還是在 ShowYItemMenu()中,或者還有其它的地方也使用了CreatePopupMenu()?有了Call Stack的信息,問題就容易了。BoundsChecker會(huì)如下報(bào)告泄漏的HMENU的信息:
Function
File
Line
CMenu::CreatePopupMenu
E:"8168"vc98"mfc"mfc"include"afxwin1.inl
1009
ShowYItemMenu
E:"testmemleak"mytest.cpp
100
這里省略了其他的函數(shù)調(diào)用
如此,我們很容易找到發(fā)生問題的函數(shù)是ShowYItemMenu()。當(dāng)使用MFC之類的類庫編程時(shí),大部分的API調(diào)用都被封裝在類庫的class里,有了Call Stack信息,我們就可以非常容易的追蹤到真正發(fā)生泄漏的代碼。
記錄Call Stack信息會(huì)使程序的運(yùn)行變得非常慢,因此默認(rèn)情況下BoundsChecker不會(huì)記錄Call Stack信息。可以按照以下的步驟打開記錄Call Stack信息的選項(xiàng)開關(guān):
1. 打開菜單:BoundsChecker|Setting…
2. 在Error Detection頁中,在Error Detection Scheme的List中選擇Custom
3. 在Category的Combox中選擇 Pointer and leak error check
4. 鉤上Report Call Stack復(fù)選框
5. 點(diǎn)擊Ok
基于Code Injection,BoundsChecker還提供了API Parameter的校驗(yàn)功能,memory over run等功能。這些功能對(duì)于程序的開發(fā)都非常有益。由于這些內(nèi)容不屬于本文的主題,所以不在此詳述了。
盡管BoundsChecker的功能如此強(qiáng)大,但是面對(duì)隱式內(nèi)存泄漏仍然顯得蒼白無力。所以接下來我們看看如何用Performance Monitor檢測(cè)內(nèi)存泄漏。
2.3.3.3 使用Performance Monitor檢測(cè)內(nèi)存泄漏
NT的內(nèi)核在設(shè)計(jì)過程中已經(jīng)加入了系統(tǒng)監(jiān)視功能,比如CPU 的使用率,內(nèi)存的使用情況,I/O操作的頻繁度等都作為一個(gè)個(gè)Counter,應(yīng)用程序可以通過讀取這些Counter了解整個(gè)系統(tǒng)的或者某個(gè)進(jìn)程的運(yùn)行 狀況。Performance Monitor就是這樣一個(gè)應(yīng)用程序。
為了檢測(cè)內(nèi)存泄漏,我們一般可以監(jiān)視Process對(duì)象的Handle Count,Virutal Bytes 和Working Set三個(gè)Counter。Handle Count記錄了進(jìn)程當(dāng)前打開的HANDLE的個(gè)數(shù),監(jiān)視這個(gè)Counter有助于我們發(fā)現(xiàn)程序是否有Handle泄漏;Virtual Bytes記錄了該進(jìn)程當(dāng)前在虛地址空間上使用的虛擬內(nèi)存的大小,NT的內(nèi)存分配采用了兩步走的方法,首先,在虛地址空間上保留一段空間,這時(shí)操作系統(tǒng)并 沒有分配物理內(nèi)存,只是保留了一段地址。然后,再提交這段空間,這時(shí)操作系統(tǒng)才會(huì)分配物理內(nèi)存。所以,Virtual Bytes一般總大于程序的Working Set。監(jiān)視Virutal Bytes可以幫助我們發(fā)現(xiàn)一些系統(tǒng)底層的問題; Working Set記錄了操作系統(tǒng)為進(jìn)程已提交的內(nèi)存的總量,這個(gè)值和程序申請(qǐng)的內(nèi)存總量存在密切的關(guān)系,如果程序存在內(nèi)存的泄漏這個(gè)值會(huì)持續(xù)增加,但是 Virtual Bytes卻是跳躍式增加的。
監(jiān)視這些Counter可以讓我們了解進(jìn)程使用內(nèi)存的情況,如果發(fā)生了泄漏,即使是隱式內(nèi)存泄漏,這些Counter的值也會(huì)持續(xù)增加。但是,我們知 道有問題卻不知道哪里有問題,所以一般使用Performance Monitor來驗(yàn)證是否有內(nèi)存泄漏,而使用BoundsChecker來找到和解決。
當(dāng)Performance Monitor顯示有內(nèi)存泄漏,而BoundsChecker卻無法檢測(cè)到,這時(shí)有兩種可能:第一種,發(fā)生了偶發(fā)性內(nèi)存泄漏。這時(shí)你要確保使用 Performance Monitor和使用BoundsChecker時(shí),程序的運(yùn)行環(huán)境和操作方法是一致的。第二種,發(fā)生了隱式的內(nèi)存泄漏。這時(shí)你要重新審查程序的設(shè)計(jì),然 后仔細(xì)研究Performance Monitor記錄的Counter的值的變化圖,分析其中的變化和程序運(yùn)行邏輯的關(guān)系,找到一些可能的原因。這是一個(gè)痛苦的過程,充滿了假設(shè)、猜想、驗(yàn) 證、失敗,但這也是一個(gè)積累經(jīng)驗(yàn)的絕好機(jī)會(huì)。
3 探討C++內(nèi)存回收
3.1 C++內(nèi)存對(duì)象大會(huì)戰(zhàn)
如果一個(gè)人自稱為程序高手,卻對(duì)內(nèi)存一無所知,那么我可以告訴你,他一定在吹牛。用C或C++寫程序,需要更多地關(guān)注內(nèi)存,這不僅僅是因?yàn)閮?nèi)存的分配 是否合理直接影響著程序的效率和性能,更為主要的是,當(dāng)我們操作內(nèi)存的時(shí)候一不小心就會(huì)出現(xiàn)問題,而且很多時(shí)候,這些問題都是不易發(fā)覺的,比如內(nèi)存泄漏, 比如懸掛指針。筆者今天在這里并不是要討論如何避免這些問題,而是想從另外一個(gè)角度來認(rèn)識(shí)C++內(nèi)存對(duì)象。
我們知道,C++將內(nèi)存劃分為三個(gè)邏輯區(qū)域:堆、棧和靜態(tài)存儲(chǔ)區(qū)。既然如此,我稱位于它們之中的對(duì)象分別為堆對(duì)象,棧對(duì)象以及靜態(tài)對(duì)象。那么這些不同的內(nèi)存對(duì)象有什么區(qū)別了?堆對(duì)象和棧對(duì)象各有什么優(yōu)劣了?如何禁止創(chuàng)建堆對(duì)象或棧對(duì)象了?這些便是今天的主題。
3.1.1 基本概念
先來看看棧。棧,一般用于存放局部變量或?qū)ο?#xff0c;如我們?cè)诤瘮?shù)定義中用類似下面語句聲明的對(duì)象:
stack_object便是一個(gè)棧對(duì)象,它的生命期是從定義點(diǎn)開始,當(dāng)所在函數(shù)返回時(shí),生命結(jié)束。
另外,幾乎所有的臨時(shí)對(duì)象都是棧對(duì)象。比如,下面的函數(shù)定義:
這 個(gè)函數(shù)至少產(chǎn)生兩個(gè)臨時(shí)對(duì)象,首先,參數(shù)是按值傳遞的,所以會(huì)調(diào)用拷貝構(gòu)造函數(shù)生成一個(gè)臨時(shí)對(duì)象object_copy1 ,在函數(shù)內(nèi)部使用的不是使用的不是object,而是object_copy1,自然,object_copy1是一個(gè)棧對(duì)象,它在函數(shù)返回時(shí)被釋放;還 有這個(gè)函數(shù)是值返回的,在函數(shù)返回時(shí),如果我們不考慮返回值優(yōu)化(NRV),那么也會(huì)產(chǎn)生一個(gè)臨時(shí)對(duì)象object_copy2,這個(gè)臨時(shí)對(duì)象會(huì)在函數(shù)返 回后一段時(shí)間內(nèi)被釋放。比如某個(gè)函數(shù)中有如下代碼:
上面的第二個(gè)語句的執(zhí)行情況是這樣的,首先函數(shù)fun返回時(shí)生成一個(gè)臨時(shí)對(duì)象object_copy2 ,然后再調(diào)用賦值運(yùn)算符執(zhí)行
看到了嗎?編譯器在我們毫無知覺的情況下,為我們生成了這么多臨時(shí)對(duì)象,而生成這些臨時(shí)對(duì)象的時(shí)間和空間的開銷可能是很大的,所以,你也許明白了,為什么對(duì)于“大”對(duì)象最好用const引用傳遞代替按值進(jìn)行函數(shù)參數(shù)傳遞了。
接下來,看看堆。堆,又叫自由存儲(chǔ)區(qū),它是在程序執(zhí)行的過程中動(dòng)態(tài)分配的,所以它最大的特性就是動(dòng)態(tài)性。在C++中,所有堆對(duì)象的創(chuàng)建和銷毀都要由程 序員負(fù)責(zé),所以,如果處理不好,就會(huì)發(fā)生內(nèi)存問題。如果分配了堆對(duì)象,卻忘記了釋放,就會(huì)產(chǎn)生內(nèi)存泄漏;而如果已釋放了對(duì)象,卻沒有將相應(yīng)的指針置為 NULL,該指針就是所謂的“懸掛指針”,再度使用此指針時(shí),就會(huì)出現(xiàn)非法訪問,嚴(yán)重時(shí)就導(dǎo)致程序崩潰。
那么,C++中是怎樣分配堆對(duì)象的?唯一的方法就是用new(當(dāng)然,用類malloc指令也可獲得C式堆內(nèi)存),只要使用new,就會(huì)在堆中分配一塊內(nèi)存,并且返回指向該堆對(duì)象的指針。
再來看看靜態(tài)存儲(chǔ)區(qū)。所有的靜態(tài)對(duì)象、全局對(duì)象都于靜態(tài)存儲(chǔ)區(qū)分配。關(guān)于全局對(duì)象,是在main()函數(shù)執(zhí)行前就分配好了的。其實(shí),在 main()函數(shù)中的顯示代碼執(zhí)行之前,會(huì)調(diào)用一個(gè)由編譯器生成的_main()函數(shù),而_main()函數(shù)會(huì)進(jìn)行所有全局對(duì)象的的構(gòu)造及初始化工作。而 在main()函數(shù)結(jié)束之前,會(huì)調(diào)用由編譯器生成的exit函數(shù),來釋放所有的全局對(duì)象。比如下面的代碼:
實(shí)際上,被轉(zhuǎn)化成這樣:
所 以,知道了這個(gè)之后,便可以由此引出一些技巧,如,假設(shè)我們要在main()函數(shù)執(zhí)行之前做某些準(zhǔn)備工作,那么我們可以將這些準(zhǔn)備工作寫到一個(gè)自定義的全 局對(duì)象的構(gòu)造函數(shù)中,這樣,在main()函數(shù)的顯式代碼執(zhí)行之前,這個(gè)全局對(duì)象的構(gòu)造函數(shù)會(huì)被調(diào)用,執(zhí)行預(yù)期的動(dòng)作,這樣就達(dá)到了我們的目的。剛才講的 是靜態(tài)存儲(chǔ)區(qū)中的全局對(duì)象,那么,局部靜態(tài)對(duì)象了?局部靜態(tài)對(duì)象通常也是在函數(shù)中定義的,就像棧對(duì)象一樣,只不過,其前面多了個(gè)static關(guān)鍵字。局部 靜態(tài)對(duì)象的生命期是從其所在函數(shù)第一次被調(diào)用,更確切地說,是當(dāng)?shù)谝淮螆?zhí)行到該靜態(tài)對(duì)象的聲明代碼時(shí),產(chǎn)生該靜態(tài)局部對(duì)象,直到整個(gè)程序結(jié)束時(shí),才銷毀該 對(duì)象。
還有一種靜態(tài)對(duì)象,那就是它作為class的靜態(tài)成員。考慮這種情況時(shí),就牽涉了一些較復(fù)雜的問題。
第一個(gè)問題是class的靜態(tài)成員對(duì)象的生命期,class的靜態(tài)成員對(duì)象隨著第一個(gè)class object的產(chǎn)生而產(chǎn)生,在整個(gè)程序結(jié)束時(shí)消亡。也就是有這樣的情況存在,在程序中我們定義了一個(gè)class,該類中有一個(gè)靜態(tài)對(duì)象作為成員,但是在程 序執(zhí)行過程中,如果我們沒有創(chuàng)建任何一個(gè)該class object,那么也就不會(huì)產(chǎn)生該class所包含的那個(gè)靜態(tài)對(duì)象。還有,如果創(chuàng)建了多個(gè)class object,那么所有這些object都共享那個(gè)靜態(tài)對(duì)象成員。
第二個(gè)問題是,當(dāng)出現(xiàn)下列情況時(shí):
請(qǐng) 注意上面標(biāo)為黑體的三條語句,它們所訪問的s_object是同一個(gè)對(duì)象嗎?答案是肯定的,它們的確是指向同一個(gè)對(duì)象,這聽起來不像是真的,是嗎?但這是 事實(shí),你可以自己寫段簡單的代碼驗(yàn)證一下。我要做的是來解釋為什么會(huì)這樣?我們知道,當(dāng)一個(gè)類比如Derived1,從另一個(gè)類比如Base繼承時(shí),那 么,可以看作一個(gè)Derived1對(duì)象中含有一個(gè)Base型的對(duì)象,這就是一個(gè)subobject。一個(gè)Derived1對(duì)象的大致內(nèi)存布局如下:
讓我們想想,當(dāng)我們將一個(gè)Derived1型的對(duì)象傳給一個(gè)接受非引用Base型參數(shù)的函數(shù)時(shí)會(huì)發(fā)生切割,那么是怎么切割的呢?相信現(xiàn)在你已經(jīng)知道 了,那就是僅僅取出了Derived1型的對(duì)象中的subobject,而忽略了所有Derived1自定義的其它數(shù)據(jù)成員,然后將這個(gè) subobject傳遞給函數(shù)(實(shí)際上,函數(shù)中使用的是這個(gè)subobject的拷貝)。
所有繼承Base類的派生類的對(duì)象都含有一個(gè)Base型的subobject(這是能用Base型指針指向一個(gè)Derived1對(duì)象的關(guān)鍵所在,自然 也是多態(tài)的關(guān)鍵了),而所有的subobject和所有Base型的對(duì)象都共用同一個(gè)s_object對(duì)象,自然,從Base類派生的整個(gè)繼承體系中的類 的實(shí)例都會(huì)共用同一個(gè)s_object對(duì)象了。上面提到的example、example1、example2的對(duì)象布局如下圖所示:
3.1.2 三種內(nèi)存對(duì)象的比較
棧對(duì)象的優(yōu)勢(shì)是在適當(dāng)?shù)臅r(shí)候自動(dòng)生成,又在適當(dāng)?shù)臅r(shí)候自動(dòng)銷毀,不需要程序員操心;而且棧對(duì)象的創(chuàng)建速度一般較堆對(duì)象快,因?yàn)榉峙涠褜?duì)象時(shí),會(huì)調(diào)用 operator new操作,operator new會(huì)采用某種內(nèi)存空間搜索算法,而該搜索過程可能是很費(fèi)時(shí)間的,產(chǎn)生棧對(duì)象則沒有這么麻煩,它僅僅需要移動(dòng)棧頂指針就可以了。但是要注意的是,通常棧 空間容量比較小,一般是1MB~2MB,所以體積比較大的對(duì)象不適合在棧中分配。特別要注意遞歸函數(shù)中最好不要使用棧對(duì)象,因?yàn)殡S著遞歸調(diào)用深度的增加, 所需的棧空間也會(huì)線性增加,當(dāng)所需棧空間不夠時(shí),便會(huì)導(dǎo)致棧溢出,這樣就會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤。
堆對(duì)象,其產(chǎn)生時(shí)刻和銷毀時(shí)刻都要程序員精確定義,也就是說,程序員對(duì)堆對(duì)象的生命具有完全的控制權(quán)。我們常常需要這樣的對(duì)象,比如,我們需要?jiǎng)?chuàng)建一 個(gè)對(duì)象,能夠被多個(gè)函數(shù)所訪問,但是又不想使其成為全局的,那么這個(gè)時(shí)候創(chuàng)建一個(gè)堆對(duì)象無疑是良好的選擇,然后在各個(gè)函數(shù)之間傳遞這個(gè)堆對(duì)象的指針,便可 以實(shí)現(xiàn)對(duì)該對(duì)象的共享。另外,相比于棧空間,堆的容量要大得多。實(shí)際上,當(dāng)物理內(nèi)存不夠時(shí),如果這時(shí)還需要生成新的堆對(duì)象,通常不會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤,而是 系統(tǒng)會(huì)使用虛擬內(nèi)存來擴(kuò)展實(shí)際的物理內(nèi)存。
接下來看看static對(duì)象。
首先是全局對(duì)象。全局對(duì)象為類間通信和函數(shù)間通信提供了一種最簡單的方式,雖然這種方式并不優(yōu)雅。一般而言,在完全的面向?qū)ο笳Z言中,是不存在全局對(duì) 象的,比如C#,因?yàn)槿謱?duì)象意味著不安全和高耦合,在程序中過多地使用全局對(duì)象將大大降低程序的健壯性、穩(wěn)定性、可維護(hù)性和可復(fù)用性。C++也完全可以 剔除全局對(duì)象,但是最終沒有,我想原因之一是為了兼容C。
其次是類的靜態(tài)成員,上面已經(jīng)提到,基類及其派生類的所有對(duì)象都共享這個(gè)靜態(tài)成員對(duì)象,所以當(dāng)需要在這些class之間或這些class objects之間進(jìn)行數(shù)據(jù)共享或通信時(shí),這樣的靜態(tài)成員無疑是很好的選擇。
接著是靜態(tài)局部對(duì)象,主要可用于保存該對(duì)象所在函數(shù)被屢次調(diào)用期間的中間狀態(tài),其中一個(gè)最顯著的例子就是遞歸函數(shù),我們都知道遞歸函數(shù)是自己調(diào)用自己 的函數(shù),如果在遞歸函數(shù)中定義一個(gè)nonstatic局部對(duì)象,那么當(dāng)遞歸次數(shù)相當(dāng)大時(shí),所產(chǎn)生的開銷也是巨大的。這是因?yàn)閚onstatic局部對(duì)象是 棧對(duì)象,每遞歸調(diào)用一次,就會(huì)產(chǎn)生一個(gè)這樣的對(duì)象,每返回一次,就會(huì)釋放這個(gè)對(duì)象,而且,這樣的對(duì)象只局限于當(dāng)前調(diào)用層,對(duì)于更深入的嵌套層和更淺露的外 層,都是不可見的。每個(gè)層都有自己的局部對(duì)象和參數(shù)。
在遞歸函數(shù)設(shè)計(jì)中,可以使用static對(duì)象替代nonstatic局部對(duì)象(即棧對(duì)象),這不僅可以減少每次遞歸調(diào)用和返回時(shí)產(chǎn)生和釋放nonstatic對(duì)象的開銷,而且static對(duì)象還可以保存遞歸調(diào)用的中間狀態(tài),并且可為各個(gè)調(diào)用層所訪問。
3.1.3 使用棧對(duì)象的意外收獲
前面已經(jīng)介紹到,棧對(duì)象是在適當(dāng)?shù)臅r(shí)候創(chuàng)建,然后在適當(dāng)?shù)臅r(shí)候自動(dòng)釋放的,也就是棧對(duì)象有自動(dòng)管理功能。那么棧對(duì)象會(huì)在什么會(huì)自動(dòng)釋放了?第一,在其 生命期結(jié)束的時(shí)候;第二,在其所在的函數(shù)發(fā)生異常的時(shí)候。你也許說,這些都很正常啊,沒什么大不了的。是的,沒什么大不了的。但是只要我們?cè)偕钊胍稽c(diǎn)點(diǎn), 也許就有意外的收獲了。
棧對(duì)象,自動(dòng)釋放時(shí),會(huì)調(diào)用它自己的析構(gòu)函數(shù)。如果我們?cè)跅?duì)象中封裝資源,而且在棧對(duì)象的析構(gòu)函數(shù)中執(zhí)行釋放資源的動(dòng)作,那么就會(huì)使資源泄漏的概率 大大降低,因?yàn)闂?duì)象可以自動(dòng)的釋放資源,即使在所在函數(shù)發(fā)生異常的時(shí)候。實(shí)際的過程是這樣的:函數(shù)拋出異常時(shí),會(huì)發(fā)生所謂的 stack_unwinding(堆棧回滾),即堆棧會(huì)展開,由于是棧對(duì)象,自然存在于棧中,所以在堆棧回滾的過程中,棧對(duì)象的析構(gòu)函數(shù)會(huì)被執(zhí)行,從而釋 放其所封裝的資源。除非,除非在析構(gòu)函數(shù)執(zhí)行的過程中再次拋出異常――而這種可能性是很小的,所以用棧對(duì)象封裝資源是比較安全的。基于此認(rèn)識(shí),我們就可以 創(chuàng)建一個(gè)自己的句柄或代理來封裝資源了。智能指針(auto_ptr)中就使用了這種技術(shù)。在有這種需要的時(shí)候,我們就希望我們的資源封裝類只能在棧中創(chuàng) 建,也就是要限制在堆中創(chuàng)建該資源封裝類的實(shí)例。
3.1.4 禁止產(chǎn)生堆對(duì)象
上面已經(jīng)提到,你決定禁止產(chǎn)生某種類型的堆對(duì)象,這時(shí)你可以自己創(chuàng)建一個(gè)資源封裝類,該類對(duì)象只能在棧中產(chǎn)生,這樣就能在異常的情況下自動(dòng)釋放封裝的資源。
那么怎樣禁止產(chǎn)生堆對(duì)象了?我們已經(jīng)知道,產(chǎn)生堆對(duì)象的唯一方法是使用new操作,如果我們禁止使用new不就行了么。再進(jìn)一步,new操作執(zhí)行時(shí)會(huì) 調(diào)用operator new,而operator new是可以重載的。方法有了,就是使new operator 為private,為了對(duì)稱,最好將operator delete也重載為private。現(xiàn)在,你也許又有疑問了,難道創(chuàng)建棧對(duì)象不需要調(diào)用new嗎?是的,不需要,因?yàn)閯?chuàng)建棧對(duì)象不需要搜索內(nèi)存,而是直 接調(diào)整堆棧指針,將對(duì)象壓棧,而operator new的主要任務(wù)是搜索合適的堆內(nèi)存,為堆對(duì)象分配空間,這在上面已經(jīng)提到過了。好,讓我們看看下面的示例代碼:
NoHashObject現(xiàn)在就是一個(gè)禁止堆對(duì)象的類了,如果你寫下如下代碼:
上 面代碼會(huì)產(chǎn)生編譯期錯(cuò)誤。好了,現(xiàn)在你已經(jīng)知道了如何設(shè)計(jì)一個(gè)禁止堆對(duì)象的類了,你也許和我一樣有這樣的疑問,難道在類NoHashObject 的定義不能改變的情況下,就一定不能產(chǎn)生該類型的堆對(duì)象了嗎?不,還是有辦法的,我稱之為“暴力破解法”。C++是如此地強(qiáng)大,強(qiáng)大到你可以用它做你想做 的任何事情。這里主要用到的是技巧是指針類型的強(qiáng)制轉(zhuǎn)換。
上面的實(shí)現(xiàn)是麻煩的,而且這種實(shí)現(xiàn)方式幾乎不會(huì)在實(shí)踐中使用,但是我還是寫出來路,因?yàn)槔斫馑?#xff0c;對(duì)于我們理解C++內(nèi)存對(duì)象是有好處的。對(duì)于上面的這么多強(qiáng)制類型轉(zhuǎn)換,其最根本的是什么了?我們可以這樣理解:
某塊內(nèi)存中的數(shù)據(jù)是不變的,而類型就是我們戴上的眼鏡,當(dāng)我們戴上一種眼鏡后,我們就會(huì)用對(duì)應(yīng)的類型來解釋內(nèi)存中的數(shù)據(jù),這樣不同的解釋就得到了不同的信息。
所謂強(qiáng)制類型轉(zhuǎn)換實(shí)際上就是換上另一副眼鏡后再來看同樣的那塊內(nèi)存數(shù)據(jù)。
另外要提醒的是,不同的編譯器對(duì)對(duì)象的成員數(shù)據(jù)的布局安排可能是不一樣的,比如,大多數(shù)編譯器將NoHashObject的ptr指針成員安排在對(duì)象空間的頭4個(gè)字節(jié),這樣才會(huì)保證下面這條語句的轉(zhuǎn)換動(dòng)作像我們預(yù)期的那樣執(zhí)行:
但是,并不一定所有的編譯器都是如此。
既然我們可以禁止產(chǎn)生某種類型的堆對(duì)象,那么可以設(shè)計(jì)一個(gè)類,使之不能產(chǎn)生棧對(duì)象嗎?當(dāng)然可以。
3.1.5 禁止產(chǎn)生棧對(duì)象
前面已經(jīng)提到了,創(chuàng)建棧對(duì)象時(shí)會(huì)移動(dòng)棧頂指針以“挪出”適當(dāng)大小的空間,然后在這個(gè)空間上直接調(diào)用對(duì)應(yīng)的構(gòu)造函數(shù)以形成一個(gè)棧對(duì)象,而當(dāng)函數(shù)返回時(shí), 會(huì)調(diào)用其析構(gòu)函數(shù)釋放這個(gè)對(duì)象,然后再調(diào)整棧頂指針收回那塊棧內(nèi)存。在這個(gè)過程中是不需要operator new/delete操作的,所以將operator new/delete設(shè)置為private不能達(dá)到目的。當(dāng)然從上面的敘述中,你也許已經(jīng)想到了:將構(gòu)造函數(shù)或析構(gòu)函數(shù)設(shè)為私有的,這樣系統(tǒng)就不能調(diào)用構(gòu) 造/析構(gòu)函數(shù)了,當(dāng)然就不能在棧中生成對(duì)象了。
這樣的確可以,而且我也打算采用這種方案。但是在此之前,有一點(diǎn)需要考慮清楚,那就是,如果我們將構(gòu)造函數(shù)設(shè)置為私有,那么我們也就不能用 new來直接產(chǎn)生堆對(duì)象了,因?yàn)閚ew在為對(duì)象分配空間后也會(huì)調(diào)用它的構(gòu)造函數(shù)啊。所以,我打算只將析構(gòu)函數(shù)設(shè)置為private。再進(jìn)一步,將析構(gòu)函數(shù) 設(shè)為private除了會(huì)限制棧對(duì)象生成外,還有其它影響嗎?是的,這還會(huì)限制繼承。
如果一個(gè)類不打算作為基類,通常采用的方案就是將其析構(gòu)函數(shù)聲明為private。
為了限制棧對(duì)象,卻不限制繼承,我們可以將析構(gòu)函數(shù)聲明為protected,這樣就兩全其美了。如下代碼所示:
接著,可以像這樣使用NoStackObject類:
呵 呵,是不是覺得有點(diǎn)怪怪的,我們用new創(chuàng)建一個(gè)對(duì)象,卻不是用delete去刪除它,而是要用destroy方法。很顯然,用戶是不習(xí)慣這種怪異的使用 方式的。所以,我決定將構(gòu)造函數(shù)也設(shè)為private或protected。這又回到了上面曾試圖避免的問題,即不用new,那么該用什么方式來生成一個(gè) 對(duì)象了?我們可以用間接的辦法完成,即讓這個(gè)類提供一個(gè)static成員函數(shù)專門用于產(chǎn)生該類型的堆對(duì)象。(設(shè)計(jì)模式中的singleton 模式就可以用這種方式實(shí)現(xiàn)。)讓我們來看看:
現(xiàn)在可以這樣使用NoStackObject類了:
現(xiàn)在感覺是不是好多了,生成對(duì)象和釋放對(duì)象的操作一致了。
3.2 淺議C++ 中的垃圾回收方法
許多 C 或者 C++ 程序員對(duì)垃圾回收嗤之以鼻,認(rèn)為垃圾回收肯定比自己來管理動(dòng)態(tài)內(nèi)存要低效,而且在回收的時(shí)候一定會(huì)讓程序停頓在那里,而如果自己控制內(nèi)存管理的話,分配和 釋放時(shí)間都是穩(wěn)定的,不會(huì)導(dǎo)致程序停頓。最后,很多 C/C++ 程序員堅(jiān)信在C/C++ 中無法實(shí)現(xiàn)垃圾回收機(jī)制。這些錯(cuò)誤的觀點(diǎn)都是由于不了解垃圾回收的算法而臆想出來的。
其實(shí)垃圾回收機(jī)制并不慢,甚至比動(dòng)態(tài)內(nèi)存分配更高效。因?yàn)槲覀兛梢灾环峙洳会尫?#xff0c;那么分配內(nèi)存的時(shí)候只需要從堆上一直的獲得新的內(nèi)存,移動(dòng)堆頂?shù)闹羔?就夠了;而釋放的過程被省略了,自然也加快了速度。現(xiàn)代的垃圾回收算法已經(jīng)發(fā)展了很多,增量收集算法已經(jīng)可以讓垃圾回收過程分段進(jìn)行,避免打斷程序的運(yùn)行 了。而傳統(tǒng)的動(dòng)態(tài)內(nèi)存管理的算法同樣有在適當(dāng)?shù)臅r(shí)間收集內(nèi)存碎片的工作要做,并不比垃圾回收更有優(yōu)勢(shì)。
而垃圾回收的算法的基礎(chǔ)通常基于掃描并標(biāo)記當(dāng)前可能被使用的所有內(nèi)存塊,從已經(jīng)被分配的所有內(nèi)存中把未標(biāo)記的內(nèi)存回收來做的。C/C++ 中無法實(shí)現(xiàn)垃圾回收的觀點(diǎn)通常基于無法正確掃描出所有可能還會(huì)被使用的內(nèi)存塊,但是,看似不可能的事情實(shí)際上實(shí)現(xiàn)起來卻并不復(fù)雜。首先,通過掃描內(nèi)存的數(shù) 據(jù),指向堆上動(dòng)態(tài)分配出來內(nèi)存的指針是很容易被識(shí)別出來的,如果有識(shí)別錯(cuò)誤,也只能是把一些不是指針的數(shù)據(jù)當(dāng)成指針,而不會(huì)把指針當(dāng)成非指針數(shù)據(jù)。這樣, 回收垃圾的過程只會(huì)漏回收掉而不會(huì)錯(cuò)誤的把不應(yīng)該回收的內(nèi)存清理。其次,如果回溯所有內(nèi)存塊被引用的根,只可能存在于全局變量和當(dāng)前的棧內(nèi),而全局變量 (包括函數(shù)內(nèi)的靜態(tài)變量)都是集中存在于 bss 段或 data段中。
垃圾回收的時(shí)候,只需要掃描 bss 段, data 段以及當(dāng)前被使用著的棧空間,找到可能是動(dòng)態(tài)內(nèi)存指針的量,把引用到的內(nèi)存遞歸掃描就可以得到當(dāng)前正在使用的所有動(dòng)態(tài)內(nèi)存了。
如果肯為你的工程實(shí)現(xiàn)一個(gè)不錯(cuò)的垃圾回收器,提高內(nèi)存管理的速度,甚至減少總的內(nèi)存消耗都是可能的。如果有興趣的話,可以搜索一下網(wǎng)上已有的關(guān)于垃圾回收的論文和實(shí)現(xiàn)了的庫,開拓視野對(duì)一個(gè)程序員尤為重要。
發(fā)表于 @ 2010年07月02日 09:27:00?|?評(píng)論(?0?)?|?舉報(bào)|?收藏
總結(jié)
- 上一篇: 高等数学同济第七版课后答案上册
- 下一篇: 阿里巴巴后台的使用体验