西西軟件下載最安全的下載網(wǎng)站、值得信賴的軟件下載站!

首頁編程開發(fā)VC|VC++ → 從內(nèi)存管 理、內(nèi)存泄漏、內(nèi)存回收探討C++內(nèi)存管理

從內(nèi)存管 理、內(nèi)存泄漏、內(nèi)存回收探討C++內(nèi)存管理

相關(guān)軟件相關(guān)文章發(fā)表評論 來源:西西整理時間:2013/1/7 22:44:58字體大。A-A+

作者:西西點擊:0次評論:0次標(biāo)簽: 內(nèi)存管理

  • 類型:電子教程大。438KB語言:中文 評分:6.0
  • 標(biāo)簽:
立即下載
3 頁 探討C++內(nèi)存回收

3 探討C++內(nèi)存回收

3.1 C++內(nèi)存對象大會戰(zhàn)

  如果一個人自稱為程序高手,卻對內(nèi)存一無所知,那么我可以告訴你,他一定在吹牛。用C或C++寫 程序,需要更多地關(guān)注內(nèi)存,這不僅僅是因為內(nèi)存的分配是否合理直接影響著程序的效率和性能,更為主要的是,當(dāng)我們操作內(nèi)存的時候一不小心就會出現(xiàn)問題,而 且很多時候,這些問題都是不易發(fā)覺的,比如內(nèi)存泄漏,比如懸掛指針。筆者今天在這里并不是要討論如何避免這些問題,而是想從另外一個角度來認(rèn)識C++內(nèi)存對象。

  我們知道,C++將內(nèi)存劃分為三個邏輯區(qū)域:堆、棧和靜態(tài)存儲區(qū)。既然如此,我稱位于它們之中的對象分別為堆對象,棧對象以及靜態(tài)對象。那么這些不同的內(nèi)存對象有什么區(qū)別了?堆對象和棧對象各有什么優(yōu)劣了?如何禁止創(chuàng)建堆對象或棧對象了?這些便是今天的主題。

3.1.1 基本概念

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

Type stack_object ; 

  stack_object便是一個棧對象,它的生命期是從定義點開始,當(dāng)所在函數(shù)返回時,生命結(jié)束。

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

Type fun(Type object);

  這個函數(shù)至少產(chǎn)生兩個臨時對象,首先,參數(shù)是按值傳遞的,所以會調(diào)用拷貝構(gòu)造函數(shù)生成一個臨時對象object_copy1 ,在函數(shù)內(nèi)部使用的不是使用的不是object,而是object_copy1,自然,object_copy1是一個棧對象,它在函數(shù)返回時被釋放;還有這個函數(shù)是值返回的,在函數(shù)返回時,如果我們不考慮返回值優(yōu)化(NRV),那么也會產(chǎn)生一個臨時對象object_copy2,這個臨時對象會在函數(shù)返回后一段時間內(nèi)被釋放。比如某個函數(shù)中有如下代碼:

Type tt ,result ; //生成兩個棧對象
tt = fun(tt); //函數(shù)返回時,生成的是一個臨時對象object_copy2

  上面的第二個語句的執(zhí)行情況是這樣的,首先函數(shù)fun返回時生成一個臨時對象object_copy2 ,然后再調(diào)用賦值運算符執(zhí)行

tt = object_copy2 ; //調(diào)用賦值運算符

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

  接下來,看看堆。堆,又叫自由存儲區(qū),它是在程序執(zhí)行的過程中動態(tài)分配的,所以它最大的特性就是動態(tài)性。在C++中,所有堆對象的創(chuàng)建和銷毀都要由程序員負(fù)責(zé),所以,如果處理不好,就會發(fā)生內(nèi)存問題。如果分配了堆對象,卻忘記了釋放,就會產(chǎn)生內(nèi)存泄漏;而如果已釋放了對象,卻沒有將相應(yīng)的指針置為NULL,該指針就是所謂的“懸掛指針”,再度使用此指針時,就會出現(xiàn)非法訪問,嚴(yán)重時就導(dǎo)致程序崩潰。

  那么,C++中是怎樣分配堆對象的?唯一的方法就是用new(當(dāng)然,用類malloc指令也可獲得C式堆內(nèi)存),只要使用new,就會在堆中分配一塊內(nèi)存,并且返回指向該堆對象的指針。

  再來看看靜態(tài)存儲區(qū)。所有的靜態(tài)對象、全局對象都于靜態(tài)存儲區(qū)分配。關(guān)于全局對象,是在main()函數(shù)執(zhí)行前就分配好了的。其實,在main()函數(shù)中的顯示代碼執(zhí)行之前,會調(diào)用一個由編譯器生成的_main()函數(shù),而_main()函數(shù)會進(jìn)行所有全局對象的的構(gòu)造及初始化工作。而在main()函數(shù)結(jié)束之前,會調(diào)用由編譯器生成的exit函數(shù),來釋放所有的全局對象。比如下面的代碼:

void main(void)
{
 … …// 顯式代碼
}

  實際上,被轉(zhuǎn)化成這樣:

void main(void)
{
 _main(); //隱式代碼,由編譯器產(chǎn)生,用以構(gòu)造所有全局對象
 … … // 顯式代碼
 … …
 exit() ; // 隱式代碼,由編譯器產(chǎn)生,用以釋放所有全局對象
}

  所以,知道了這個之后,便可以由此引出一些技巧,如,假設(shè)我們要在main()函數(shù)執(zhí)行之前做某些準(zhǔn)備工作,那么我們可以將這些準(zhǔn)備工作寫到一個自定義的全局對象的構(gòu)造函數(shù)中,這樣,在main()函數(shù)的顯式代碼執(zhí)行之前,這個全局對象的構(gòu)造函數(shù)會被調(diào)用,執(zhí)行預(yù)期的動作,這樣就達(dá)到了我們的目的。 剛才講的是靜態(tài)存儲區(qū)中的全局對象,那么,局部靜態(tài)對象了?局部靜態(tài)對象通常也是在函數(shù)中定義的,就像棧對象一樣,只不過,其前面多了個static關(guān)鍵字。局部靜態(tài)對象的生命期是從其所在函數(shù)第一次被調(diào)用,更確切地說,是當(dāng)?shù)谝淮螆?zhí)行到該靜態(tài)對象的聲明代碼時,產(chǎn)生該靜態(tài)局部對象,直到整個程序結(jié)束時,才銷毀該對象。

  還有一種靜態(tài)對象,那就是它作為class的靜態(tài)成員。考慮這種情況時,就牽涉了一些較復(fù)雜的問題。

  第一個問題是class的靜態(tài)成員對象的生命期,class的靜態(tài)成員對象隨著第一個class object的產(chǎn)生而產(chǎn)生,在整個程序結(jié)束時消亡。也就是有這樣的情況存在,在程序中我們定義了一個class,該類中有一個靜態(tài)對象作為成員,但是在程序執(zhí)行過程中,如果我們沒有創(chuàng)建任何一個該class object,那么也就不會產(chǎn)生該class所包含的那個靜態(tài)對象。還有,如果創(chuàng)建了多個class object,那么所有這些object都共享那個靜態(tài)對象成員。

  第二個問題是,當(dāng)出現(xiàn)下列情況時:

 class Base
{
 public:
  static Type s_object ;
}
class Derived1 : public Base / / 公共繼承
{
 … …// other data
}
class Derived2 : public Base / / 公共繼承
{
 … …// other data
}
Base example ;
Derivde1 example1 ;
Derivde2 example2 ;
example.s_object = …… ;
example1.s_object = …… ;
example2.s_object = …… ; 

  請注意上面標(biāo)為黑體的三條語句,它們所訪問的s_object是同一個對象嗎?答案是肯定的,它們的確是指向同一個對象,這聽起來不像是真的,是嗎?但這是事實,你可以自己寫段簡單的代碼驗證一下。我要做的是來解釋為什么會這樣? 我們知道,當(dāng)一個類比如Derived1,從另一個類比如Base繼承時,那么,可以看作一個Derived1對象中含有一個Base型的對象,這就是一個subobject。一個Derived1對象的大致內(nèi)存布局如下:

  

  讓我們想想,當(dāng)我們將一個Derived1型的對象傳給一個接受非引用Base型參數(shù)的函數(shù)時會發(fā)生切割,那么是怎么切割的呢?相信現(xiàn)在你已經(jīng)知道了,那就是僅僅取出了Derived1型的對象中的subobject,而忽略了所有Derived1自定義的其它數(shù)據(jù)成員,然后將這個subobject傳遞給函數(shù)(實際上,函數(shù)中使用的是這個subobject的拷貝)。

  所有繼承Base類的派生類的對象都含有一個Base型的subobject(這是能用Base型指針指向一個Derived1對象的關(guān)鍵所在,自然也是多態(tài)的關(guān)鍵了),而所有的subobject和所有Base型的對象都共用同一個s_object對象,自然,從Base類派生的整個繼承體系中的類的實例都會共用同一個s_object對象了。上面提到的example、example1、example2的對象布局如下圖所示:

3.1.2 三種內(nèi)存對象的比較

  棧對象的優(yōu)勢是在適當(dāng)?shù)臅r候自動生成,又在適當(dāng)?shù)臅r候自動銷毀,不需要程序員操心;而且棧對象的創(chuàng)建速度一般較堆對象快,因為分配堆對象時,會調(diào)用operator new操作,operator new會采用某種內(nèi)存空間搜索算法,而該搜索過程可能是很費時間的,產(chǎn)生棧對象則沒有這么麻煩,它僅僅需要移動棧頂指針就可以了。但是要注意的是,通常?臻g容量比較小,一般是1MB~2MB,所以體積比較大的對象不適合在棧中分配。特別要注意遞歸函數(shù)中最好不要使用棧對象,因為隨著遞歸調(diào)用深度的增加,所需的棧空間也會線性增加,當(dāng)所需?臻g不夠時,便會導(dǎo)致棧溢出,這樣就會產(chǎn)生運行時錯誤。

  堆對象,其產(chǎn)生時刻和銷毀時刻都要程序員精確定義,也就是說,程序員對堆對象的 生命具有完全的控制權(quán)。我們常常需要這樣的對象,比如,我們需要創(chuàng)建一個對象,能夠被多個函數(shù)所訪問,但是又不想使其成為全局的,那么這個時候創(chuàng)建一個堆 對象無疑是良好的選擇,然后在各個函數(shù)之間傳遞這個堆對象的指針,便可以實現(xiàn)對該對象的共享。另外,相比于?臻g,堆的容量要大得多。實際上,當(dāng)物理內(nèi)存 不夠時,如果這時還需要生成新的堆對象,通常不會產(chǎn)生運行時錯誤,而是系統(tǒng)會使用虛擬內(nèi)存來擴展實際的物理內(nèi)存。

接下來看看static對象。

  首先是全局對象。全局對象為類間通信和函數(shù)間通信提供了一種最簡單的方式,雖然這種方式并不優(yōu)雅。一般而言,在完全的面向?qū)ο笳Z言中,是不存在全局對象的,比如C#,因為全局對象意味著不安全和高耦合,在程序中過多地使用全局對象將大大降低程序的健壯性、穩(wěn)定性、可維護(hù)性和可復(fù)用性。C++也完全可以剔除全局對象,但是最終沒有,我想原因之一是為了兼容C。

  其次是類的靜態(tài)成員,上面已經(jīng)提到,基類及其派生類的所有對象都共享這個靜態(tài)成員對象,所以當(dāng)需要在這些class之間或這些class objects之間進(jìn)行數(shù)據(jù)共享或通信時,這樣的靜態(tài)成員無疑是很好的選擇。

  接著是靜態(tài)局部對象,主要可用于保存該對象所在函數(shù)被屢次調(diào)用期間的中間狀態(tài),其中一個最顯著的例子就是遞歸函數(shù),我們都知道遞歸函數(shù)是自己調(diào)用自己的函數(shù),如果在遞歸函數(shù)中定義一個nonstatic局部對象,那么當(dāng)遞歸次數(shù)相當(dāng)大時,所產(chǎn)生的開銷也是巨大的。這是因為nonstatic局部對象是棧對象,每遞歸調(diào)用一次,就會產(chǎn)生一個這樣的對象,每返回一次,就會釋放這個對象,而且,這樣的對象只局限于當(dāng)前調(diào)用層,對于更深入的嵌套層和更淺露的外層,都是不可見的。每個層都有自己的局部對象和參數(shù)。

  在遞歸函數(shù)設(shè)計中,可以使用static對象替代nonstatic局部對象(即棧對象),這不僅可以減少每次遞歸調(diào)用和返回時產(chǎn)生和釋放nonstatic對象的開銷,而且static對象還可以保存遞歸調(diào)用的中間狀態(tài),并且可為各個調(diào)用層所訪問。

3.1.3 使用棧對象的意外收獲

  前面已經(jīng)介紹到,棧對象是在適當(dāng)?shù)臅r候創(chuàng)建,然后在適當(dāng)?shù)臅r候自動釋放的,也就 是棧對象有自動管理功能。那么棧對象會在什么會自動釋放了?第一,在其生命期結(jié)束的時候;第二,在其所在的函數(shù)發(fā)生異常的時候。你也許說,這些都很正常 啊,沒什么大不了的。是的,沒什么大不了的。但是只要我們再深入一點點,也許就有意外的收獲了。

  棧對象,自動釋放時,會調(diào)用它自己的析構(gòu)函數(shù)。如果我們在棧對象中封裝資源,而 且在棧對象的析構(gòu)函數(shù)中執(zhí)行釋放資源的動作,那么就會使資源泄漏的概率大大降低,因為棧對象可以自動的釋放資源,即使在所在函數(shù)發(fā)生異常的時候。實際的過 程是這樣的:函數(shù)拋出異常時,會發(fā)生所謂的stack_unwinding(堆 ;貪L),即堆棧會展開,由于是棧對象,自然存在于棧中,所以在堆棧回滾的過程中,棧對象的析構(gòu)函數(shù)會被執(zhí)行,從而釋放其所封裝的資源。除非,除非在析構(gòu) 函數(shù)執(zhí)行的過程中再次拋出異常――而這種可能性是很小的,所以用棧對象封裝資源是比較安全的;诖苏J(rèn)識,我們就可以創(chuàng)建一個自己的句柄或代理來封裝資源 了。智能指針(auto_ptr)中就使用了這種技術(shù)。在有這種需要的時候,我們就希望我們的資源封裝類只能在棧中創(chuàng)建,也就是要限制在堆中創(chuàng)建該資源封裝類的實例。

3.1.4 禁止產(chǎn)生堆對象

  上面已經(jīng)提到,你決定禁止產(chǎn)生某種類型的堆對象,這時你可以自己創(chuàng)建一個資源封裝類,該類對象只能在棧中產(chǎn)生,這樣就能在異常的情況下自動釋放封裝的資源。

  那么怎樣禁止產(chǎn)生堆對象了?我們已經(jīng)知道,產(chǎn)生堆對象的唯一方法是使用new操作,如果我們禁止使用new不就行了么。再進(jìn)一步,new操作執(zhí)行時會調(diào)用operator new,而operator new是可以重載的。方法有了,就是使new operator 為private,為了對稱,最好將operator delete也重載為private,F(xiàn)在,你也許又有疑問了,難道創(chuàng)建棧對象不需要調(diào)用new嗎?是的,不需要,因為創(chuàng)建棧對象不需要搜索內(nèi)存,而是直接調(diào)整堆棧指針,將對象壓棧,而operator new的主要任務(wù)是搜索合適的堆內(nèi)存,為堆對象分配空間,這在上面已經(jīng)提到過了。好,讓我們看看下面的示例代碼:

#include <stdlib.h> //需要用到C式內(nèi)存分配函數(shù)
class Resource ; //代表需要被封裝的資源類
class NoHashObject
{
 private:
  Resource* ptr ;//指向被封裝的資源
  ... ... //其它數(shù)據(jù)成員
  void* operator new(size_t size) //非嚴(yán)格實現(xiàn),僅作示意之用
  {
   return malloc(size) ;
  }
  void operator delete(void* pp) //非嚴(yán)格實現(xiàn),僅作示意之用
  {
   free(pp) ;
  }
 public:
  NoHashObject()
  {
   //此處可以獲得需要封裝的資源,并讓ptr指針指向該資源
   ptr = new Resource() ;
  }
  ~NoHashObject()
  {
   delete ptr ; //釋放封裝的資源
  }
}; 
  NoHashObject現(xiàn)在就是一個禁止堆對象的類了,如果你寫下如下代碼:
NoHashObject* fp = new NoHashObject() ; //編譯期錯誤!
delete fp ; 

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

void main(void)
{
 char* temp = new char[sizeof(NoHashObject)] ;
 //強制類型轉(zhuǎn)換,現(xiàn)在ptr是一個指向NoHashObject對象的指針
 NoHashObject* obj_ptr = (NoHashObject*)temp ;
 temp = NULL ; //防止通過temp指針修改NoHashObject對象
 //再一次強制類型轉(zhuǎn)換,讓rp指針指向堆中NoHashObject對象的ptr成員
 Resource* rp = (Resource*)obj_ptr ;
 //初始化obj_ptr指向的NoHashObject對象的ptr成員
 rp = new Resource() ;
 //現(xiàn)在可以通過使用obj_ptr指針使用堆中的NoHashObject對象成員了
 ... ...
 delete rp ;//釋放資源
 temp = (char*)obj_ptr ;
 obj_ptr = NULL ;//防止懸掛指針產(chǎn)生
 delete [] temp ;//釋放NoHashObject對象所占的堆空間。

  上面的實現(xiàn)是麻煩的,而且這種實現(xiàn)方式幾乎不會在實踐中使用,但是我還是寫出來路,因為理解它,對于我們理解C++內(nèi)存對象是有好處的。對于上面的這么多強制類型轉(zhuǎn)換,其最根本的是什么了?我們可以這樣理解:

  某塊內(nèi)存中的數(shù)據(jù)是不變的,而類型就是我們戴上的眼鏡,當(dāng)我們戴上一種眼鏡后,我們就會用對應(yīng)的類型來解釋內(nèi)存中的數(shù)據(jù),這樣不同的解釋就得到了不同的信息。

  所謂強制類型轉(zhuǎn)換實際上就是換上另一副眼鏡后再來看同樣的那塊內(nèi)存數(shù)據(jù)。

  另外要提醒的是,不同的編譯器對對象的成員數(shù)據(jù)的布局安排可能是不一樣的,比如,大多數(shù)編譯器將NoHashObject的ptr指針成員安排在對象空間的頭4個字節(jié),這樣才會保證下面這條語句的轉(zhuǎn)換動作像我們預(yù)期的那樣執(zhí)行:

Resource* rp = (Resource*)obj_ptr ; 

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

  既然我們可以禁止產(chǎn)生某種類型的堆對象,那么可以設(shè)計一個類,使之不能產(chǎn)生棧對象嗎?當(dāng)然可以。

3.1.5 禁止產(chǎn)生棧對象

  前面已經(jīng)提到了,創(chuàng)建棧對象時會移動棧頂指針以“挪出”適當(dāng)大小的空間,然后在這個空間上直接調(diào)用對應(yīng)的構(gòu)造函數(shù)以形成一個棧對象,而當(dāng)函數(shù)返回時,會調(diào)用其析構(gòu)函數(shù)釋放這個對象,然后再調(diào)整棧頂指針收回那塊棧內(nèi)存。在這個過程中是不需要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)然就不能在棧中生成對象了。

  這樣的確可以,而且我也打算采用這種方案。但是在此之前,有一點需要考慮清楚,那就是,如果我們將構(gòu)造函數(shù)設(shè)置為私有,那么我們也就不能用new來直接產(chǎn)生堆對象了,因為new在為對象分配空間后也會調(diào)用它的構(gòu)造函數(shù)啊。所以,我打算只將析構(gòu)函數(shù)設(shè)置為private。再進(jìn)一步,將析構(gòu)函數(shù)設(shè)為private除了會限制棧對象生成外,還有其它影響嗎?是的,這還會限制繼承。

  如果一個類不打算作為基類,通常采用的方案就是將其析構(gòu)函數(shù)聲明為private。

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

class NoStackObject
{
 protected:
  ~NoStackObject() { }
 public:
  void destroy()
  {
   delete this ;//調(diào)用保護(hù)析構(gòu)函數(shù)
  }
}; 

  接著,可以像這樣使用NoStackObject類:

NoStackObject* hash_ptr = new NoStackObject() ;
... ... //對hash_ptr指向的對象進(jìn)行操作
hash_ptr->destroy() ; 

  呵呵,是不是覺得有點怪怪的,我們用new創(chuàng)建一個對象,卻不是用delete去刪除它,而是要用destroy方法。很顯然,用戶是不習(xí)慣這種怪異的使用方式的。所以,我決定將構(gòu)造函數(shù)也設(shè)為private或protected。這又回到了上面曾試圖避免的問題,即不用new,那么該用什么方式來生成一個對象了?我們可以用間接的辦法完成,即讓這個類提供一個static成員函數(shù)專門用于產(chǎn)生該類型的堆對象。(設(shè)計模式中的singleton模式就可以用這種方式實現(xiàn)。)讓我們來看看:

class NoStackObject
{
 protected:
  NoStackObject() { }
  ~NoStackObject() { }
 public:
  static NoStackObject* creatInstance()
  {
   return new NoStackObject() ;//調(diào)用保護(hù)的構(gòu)造函數(shù)
  }
  void destroy()
  {
   delete this ;//調(diào)用保護(hù)的析構(gòu)函數(shù)
  }
};

  現(xiàn)在可以這樣使用NoStackObject類了:

NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
... ... //對hash_ptr指向的對象進(jìn)行操作
hash_ptr->destroy() ;
hash_ptr = NULL ; //防止使用懸掛指針 

現(xiàn)在感覺是不是好多了,生成對象和釋放對象的操作一致了。

    相關(guān)評論

    閱讀本文后您有什么感想? 已有人給出評價!

    • 8 喜歡喜歡
    • 3 頂
    • 1 難過難過
    • 5 囧
    • 3 圍觀圍觀
    • 2 無聊無聊

    熱門評論

    最新評論

    發(fā)表評論 查看所有評論(0)

    昵稱:
    表情: 高興 可 汗 我不要 害羞 好 下下下 送花 屎 親親
    字?jǐn)?shù): 0/500 (您的評論需要經(jīng)過審核才能顯示)
    推薦文章

    沒有數(shù)據(jù)

      沒有數(shù)據(jù)
    最新文章
      沒有數(shù)據(jù)