1. 程式人生 > 程式設計 >c++如何控制物件的建立方式(禁止建立棧物件or堆物件)和建立的數量

c++如何控制物件的建立方式(禁止建立棧物件or堆物件)和建立的數量

我們知道,C++將記憶體劃分為三個邏輯區域:堆、棧和靜態儲存區。既然如此,我稱位於它們之中的物件分別為堆物件,棧物件以及靜態物件。通常情況下,物件建立在堆上還是在棧上,建立多少個,這都是沒有限制的。但是有時會遇到一些特殊需求。

1.禁止建立棧物件

禁止建立棧物件,意味著只能在堆上建立物件。建立棧物件時會移動棧頂指標以“挪出”適當大小的空間,然後在這個空間上直接呼叫類的建構函式以形成一個棧物件。而當棧物件生命週期結束,如棧物件所在函式返回時,會呼叫其解構函式釋放這個物件,然後再調整棧頂指標收回那塊棧記憶體。在這個過程中是不需要operator new/delete操作的,所以將operator new/delete設定為private不能達到目的。

可以將建構函式或解構函式設為私有的,這樣系統就不能呼叫構造/析構函數了,當然就不能在棧中生成物件了。這樣的確可以,但有一點需要注意,那就是如果我們將建構函式設定為私有,那麼我們也就不能用new來直接產生堆物件了,因為new在為物件分配空間後也會呼叫它的建構函式。所以,如果將建構函式和解構函式都宣告為private會帶來較大的副作用,最好的方法是將解構函式宣告為private,而建構函式保持為public。

再進一步,將解構函式設為private除了會限制棧物件生成外,還有其它影響嗎?是的,這還會限制繼承。如果一個類不打算作為基類,通常採用的方案就是將其解構函式宣告為private。為了限制棧物件,卻不限制繼承,我們可以將解構函式宣告為protected,這樣就兩全其美了。如下程式碼所示:

class NoStackObject{ 
protected: 
 ~NoStackObject(){} 
public: 
 void destroy(){ 
  delete this ;//呼叫保護解構函式 
 } 
};

上面的類在建立棧物件時,如NoStackObject obj;時編譯將會報錯,而採用new的方式,編譯就會通過。需要注意一點的是,通過new建立堆物件時,在手動釋放物件記憶體時,我們需要呼叫其解構函式,這時就需要一點技巧來輔助——引入偽解構函式destory,如上面的程式碼所示。

方法拓展。

仔細一看,我們會發現上面的方法讓人彆扭。我們用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 ; //防止使用懸掛指標

現在感覺是不是好多了,生成物件和釋放物件的操作一致了。

2.禁止建立堆物件

我們已經知道,產生堆物件的唯一方法是使用new操作,如果我們禁止使用new不就行了麼。再進一步,new操作執行時會呼叫operator new,而operator new是可以過載的。方法有了,就是使new operator 為private,為了對稱,最好將operator delete也過載為private。

class NoStackObject{
private:
 static void* operator new(size_t size);
 static void operator delete(void* ptr);
};

//使用者程式碼
NoStackObject obj0;    //OK
static NoStackObject obj1;  //OK
NoStackObject * pObj2 = new NoStackObject; //ERROR

如果也想禁止堆物件陣列,可以把operator new[]和operator delete[]也宣告為private。

這裡同樣在繼承時存在問題,如果派生類改寫了operator new和operator delete並宣告為public,則基類中原有的private版本將失效,參考如下程式碼:

class NoStackObject{
protected:
 static void* operator new(size_t size);
 static void operator delete(void* ptr);
};


class NoStackObjectSon:public NoStackObject{
public:
 static void* operator new(size_t size){ //非嚴格實現,僅作示意之用 
  return malloc(size);
 };
 static void operator delete(void* ptr){ //非嚴格實現,僅作示意之用 
  free(ptr);
 };
};

//使用者程式碼
NoStackObjectSon* pObj2 = new NoStackObjectSon; //OK

3.控制例項化物件的個數

在遊戲設計中,我們採用類CGameWorld作為遊戲場景的抽象描述。然而在遊戲執行過程中,遊戲場景只有一個,也就是對CGameWorld物件的只有一個。對於物件的例項化,有一點是十分確定的:要呼叫建構函式。所以,如果想控制CGameWorld的例項化物件只有一個,最簡單的方法就是將建構函式宣告為private,同時提供一個static物件。如下:

class CGameWorld
{
public:
 bool Init();
 void Run();
private:
 CGameWorld();
 CGameWorld(const CGameWorld& rhs);

 friend CGameWorld& GetSingleGameWorld();
};

CGameWorld& GetSingleGameWorld()
{
 static CGameWorld s_game_world;
 return s_game_world;
}

這個設計有三個要點:

(1)類的建構函式是private,阻止物件的建立;
(2)GetSingleGameWorld函式被宣告為友元,避免了私有建構函式引起的限制;
(3)s_game_world為一個靜態物件,物件唯一。

當用到CGameWorld的唯一例項化物件時,可以如下:

GetSingleGameWorld().Init();
GetSingleGameWorld().Run();

如果有人對GetSingleGameWorld是一個全域性函式有些不爽,或者不想使用友元,將其宣告為類CGameWorld的靜態函式也可以達到目的,如下:

class CGameWorld
{
public:
 bool Init();
 void Run();
 static CGameWorld& GetSingleGameWorld();
private:
 CGameWorld();
 CGameWorld(const CGameWorld& rhs);
};

這就是設計模式中著名的單件模式:保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。

如果我們想讓物件產生的個數不是一個,而是最大為N(N>0)個。可以在類內部設定一個靜態計數變數,在呼叫建構函式時,該變數加1,當呼叫解構函式時,該變數減1。如下:

class CObject
{
public:
 CObject();
 ~CObject();
private:
 static size_t m_nObjCount;
 ...
};

CObject::CObject()
{
 if (m_nObjCount > N)
  throw;
 m_nObjCount++;
}

CObject::~CObject()
{
 m_nObjCount--;
}
size_t CObject::m_nObjCount;

掌握控制類的例項化物件個數的方法。當例項化物件唯一時,採用設計模式中的單件模式;當例項化物件為N(N>0)個時,設定計數變數是一個思路。

閱讀上面的示例程式碼還需要注意丟擲異常時沒有物件,即throw後沒有物件,有兩種含義:

(1)如果throw;在catch塊中或被catch塊呼叫的函式中出現,表示重新丟擲異常。throw;表示式將重新丟擲當前正在處理的異常。 我們建議採用該形式,因為這將保留原始異常的多型型別資訊。重新引發的異常物件是原始異常物件,而不是副本。

(2)如果throw;出現在非catch塊中,表示丟擲不能被捕獲的異常,即使catch(…)也不能將其補捕獲。

4.小結

堆物件,棧物件以及靜態物件統稱為記憶體物件,如果要把記憶體物件理解的更為深入,推薦看看《深入探索C++物件模型》這本書。

以上就是c++如何控制物件的建立方式(禁止建立棧物件or堆物件)和建立的數量的詳細內容,更多關於c++控制物件的建立方式與數量的資料請關注我們其它相關文章!