禁止產生棧對象;禁止產生堆對象
-
禁止產生堆對象
-
上面已經提到,你決定禁止產生某種類型的堆對象,這時你可以自己創建一個資源封裝類,該類對象只能在棧中產生,這樣就能在異常的情況下自動釋放封裝的資源。
那麼怎樣禁止產生堆對象了?我們已經知道,產生堆對象的唯一方法是使用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++是如此地強大,強大到你可以用它做你想做的任何事情。這裏主要用到的是技巧是指針類型的強制轉換。
void main(void)
{
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 ;
但是,並不一定所有的編譯器都是如此。 既然我們可以禁止產生某種類型的堆對象,那麼可以設計一個類,使之不能產生棧對象嗎?當然可以。
-
禁止產生棧對象
-
創建棧對象時會移動棧頂指針以“挪出”適當大小的空間,然後在這個空間上直接調用對應的構造函數以形成一個棧對象,而當函數返回時,會調用其析構函數釋放這個對象,然後再調整棧頂指針收回那塊棧內存。在這個過程中是不需要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 ; //防止使用懸掛指針 現在感覺是不是好多了,生成對象和釋放對象的操作一致了
|