只在堆上或只在棧上定義物件

sunmenggmail發表於2012-04-26
程式碼:
  1. class OnlyHeapClass  
  2. {  
  3. public:  
  4.     OnlyHeapClass()  
  5.     {  
  6.     }  
  7.   
  8.     void Destroy()  
  9.     {  
  10.         delete this// 等效於"OnlyHeapClass::~OnlyHeapClass();", 寫  
  11.                      // 成"OnlyHeapClass::~OnlyHeapClass();"更容易理  
  12.                      // 解public成員函式呼叫private解構函式.  
  13.     }  
  14.   
  15. private:  
  16.     ~OnlyHeapClass()  
  17.     {  
  18.     }  
  19. };  
  20.   
  21. int main()  
  22. {  
  23.     OnlyHeapClass *pInst = new OnlyHeapClass;  
  24.   
  25.     pInst ->Destroy(); // 如果類中沒有定義Destroy()函式, 而在這裡用"delete pInst;"代  
  26.                        // 替"pInst->Destroy();", 則會報錯. 因為"delete pInst;"會去調  
  27.                        // 用類的解構函式, 而在類域外呼叫類的private成員函式必然會報錯.  
  28.   
  29.     return 0;  
  30. }  
總結:  把解構函式定義為private訪問許可權, 就可以保證只能在堆(heap)上建立(new)一個新的類物件.
    
    原因是C++是一個靜態繫結的語言. 在編譯過程中, 所有的非虛擬函式呼叫都必須分析完成. 即使是虛擬函式, 也需檢查可訪問性. 因些, 當在棧(stack)上生成物件時, 物件會自動析構, 也就說解構函式必須可以訪問. 而堆上生成物件, 由於析構時機由程式設計師控制, 所以不一定需要解構函式. 保證了不能在棧上生成物件後, 需要證明能在堆上生成它. 這裡OnlyHeapClass與一般物件唯一的區別在於它的解構函式為私有, delete操作會呼叫解構函式, 所以不能編譯.

    那麼如何釋放它呢? 答案也很簡單, 提供一個成員函式, 完成delete操作. 在成員函式中, 解構函式是可以訪問的, 當然detele操作也是可以編譯通過.
  1. void OnlyHeapClass::Destroy()   
  2. {   
  3.     delete this;   
  4. }   

    解構函式私有化的類的設計可以保證只能用new命令在堆(heap)中建立物件, 只能動態的去建立物件, 這樣可以自由的控制物件的生命週期. 但是, 這樣的類需要提供建立和撤銷的公共介面. 

    另外過載delete, new為私有可以達到要求物件建立於棧上的目的, 用placement new也可以建立在棧上.

  1. ///////////////// 下面灰色字型系轉載幫助理解之用 /////////////////  

 
四.禁止產生堆物件
    上面已經提到, 你決定禁止產生某種型別的堆物件, 這時你可以自己建立一個資源封裝類, 該類物件只能在棧中產生, 這樣就能在異常的情況下自動釋放封裝的資源.
    那麼怎樣禁止產生堆物件了? 我們已經知道, 產生堆物件的唯一方法是使用new操作, 如果我們禁止使用new不就行了麼. 再進一步, new操作執行時會呼叫operator new, 而operator new是可以過載的. 方法有了, 就是使new operator為private, 為了對稱, 最好將operator delete也過載為private. 現在, 你也許又有疑問了, 難道建立棧物件不需要呼叫new嗎? 是的, 不需要, 因為建立棧物件不需要搜尋記憶體, 而是直接調整堆疊指標, 將物件壓棧, 而operator new的主要任務是搜尋合適的堆記憶體, 為堆物件分配空間, 這在上面已經提到過了. 好, 讓我們看看下面的示例程式碼:   
  #include <stdlib.h>   // 需要用到C式記憶體分配函式   
  class Resource ;   // 代表需要被封裝的資源類   
  class NoHashObject   
  {   
   private:   
    Resource *ptr ; // 指向被封裝的資源   
    // ...  //其它資料成員 
  
    void*   operator   new(size_t   size) //非嚴格實現, 僅作示意之用   
    {   
     return malloc(size);   
    } 
  
    void operator delete(void* pp) //非嚴格實現, 僅作示意之用   
    {   
     free(pp);   
    }
  
   public:   
    NoHashObject()   
    {   
     // 此處可以獲得需要封裝的資源, 並讓ptr指標指向該資源   
     ptr = new Resource();   
    } 
  
    ~NoHashObject()   
    {   
     delete ptr;   // 釋放封裝的資源   
    }   
  };     
    
    NoHashObject現在就是一個禁止堆物件的類了, 如果你寫下如下程式碼:    
    
  NoHashObject* fp = new NoHashObject(); // 編譯期錯誤!   
  delete fp; 
    
    上面程式碼會產生編譯期錯誤. 好了, 現在你已經知道了如何設計一個禁止堆物件的類了, 你也許和我一樣有這樣的疑問, 難道在類NoHashObject的定義不能改變的情況下, 就一定不能產生該型別的堆物件了嗎? 不, 還是有辦法的, 我稱之為“暴力破解法”. C++是如此地強大, 強大到你可以用它做你想做的任何事情. 這裡主要用到的是技巧是指標型別的強制轉換.    
    
  int main()   
  {   
   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物件所佔的堆空間.  
    
    return 0;    
  }    
    上面的實現是麻煩的, 而且這種實現方式幾乎不會在實踐中使用, 但是我還是寫出來路, 因為理解它, 對於我們理解C++記憶體物件是有好處的. 對於上面的這麼多強制型別轉換, 其最根本的是什麼了? 我們可以這樣理解:    
    
    某塊記憶體中的資料是不變的, 而型別就是我們戴上的眼鏡, 當我們戴上一種眼鏡後, 我們就會用對應的型別來解釋記憶體中的資料, 這樣不同的解釋就得到了不同的資訊.    
    
    所謂強制型別轉換實際上就是換上另一副眼鏡後再來看同樣的那塊記憶體資料.    
        
    另外要提醒的是, 不同的編譯器對物件的成員資料的佈局安排可能是不一樣的, 比如, 大多數編譯器將NoHashObject的ptr指標成員安排在物件空間的頭4個位元組, 這樣才會保證下面這條語句的轉換動作像我們預期的那樣執行:    
    
  Resource*   rp   =   (Resource*)obj_ptr   ;     
    
    但是, 並不一定所有的編譯器都是如此.    
    
    既然我們可以禁止產生某種型別的堆物件, 那麼可以設計一個類, 使之不能產生棧物件嗎? 當然可以.    
    
    五.禁止產生棧物件   
    
    前面已經提到了, 建立棧物件時會移動棧頂指標以“挪出”適當大小的空間, 然後在這個空間上直接呼叫對應的建構函式以形成一個棧物件, 而當函式返回時, 會呼叫其解構函式釋放這個物件, 然後再調整棧頂指標收回那塊棧記憶體. 在這個過程中是不需要operator   new/delete操作的, 所以將operator   new/delete設定為private不能達到目的. 當然從上面的敘述中, 你也許已經想到了: 將建構函式或解構函式設為私有的, 這樣系統就不能呼叫構造/解構函式了, 當然就不能在棧中生成物件了.    
    
    這樣的確可以, 而且我也打算採用這種方案. 但是在此之前, 有一點需要考慮清楚,那就是, 如果我們將建構函式設定為私有, 那麼我們也就不能用new來直接產生堆物件了, 因為new在為物件分配空間後也會呼叫它的建構函式啊. 所以, 我打算只將解構函式設定為private. 再進一步, 將解構函式設為private除了會限制棧物件生成外, 還有其它影響嗎? 是的, 這還會限制繼承.    
    
    如果一個類不打算作為基類, 通常採用的方案就是將其解構函式宣告為private.    
    
    為了限制棧物件, 卻不限制繼承, 我們可以將解構函式宣告為protected, 這樣就兩全其美了. 如下程式碼所示:    
    
  class   NoStackObject   
  {   
   protected:   
    ~NoStackObject()   {   }   
   public:   
    void   destroy()   
    {   
     delete   this   ;//呼叫保護解構函式   
    }   
  };     
    
    接著, 可以像這樣使用NoStackObject類:    
    
  NoStackObject*   hash_ptr   =   new   NoStackObject()   ;   
  ...   ...   //對hash_ptr指向的物件進行操作   
  hash_ptr->destroy()   ;     
    
    呵呵, 是不是覺得有點怪怪的, 我們用new建立一個物件, 卻不是用delete去刪除它, 而是要用destroy方法. 很顯然, 使用者是不習慣這種怪異的使用方式的. 所以, 我決定將建構函式也設為private或protected. 這又回到了上面曾試圖避免的問題, 即不用new, 那麼該用什麼方式來生成一個物件了? 我們可以用間接的辦法完成, 即讓這個類提供一個static成員函式專門用於產生該型別的堆物件. (設計模式中的singleton模式就可以用這種方式實現. )讓我們來看看:    
    
  class   NoStackObject   
  {   
   protected:   
    NoStackObject()   {   }   
    ~NoStackObject()   {   }   
   public:   
    static   NoStackObject*   creatInstance()   
    {   
     return   new   NoStackObject()   ;//呼叫保護的建構函式   
    }   
    void   destroy()   
    {   
     delete   this   ;//呼叫保護的解構函式   
    }   
  };     
    
    現在可以這樣使用NoStackObject類了:    
    
  NoStackObject*   hash_ptr   =   NoStackObject::creatInstance()   ;   
  ...   ...   //對hash_ptr指向的物件進行操作   
  hash_ptr->destroy()   ;   
  hash_ptr   =   NULL   ;   //防止使用懸掛指標     
    
    現在感覺是不是好多了, 生成物件和釋放物件的操作一致了.   

相關文章