面向對象的基礎結構設計
一個遊戲引擎是一個巨大而複雜的軟體系統。面向對象的軟體工程和類庫設計方法能夠給這樣的大型的軟體系統提供很好的支援。這個附錄将提供面向對象結構設計的基本問題的一個回顧。另外,在遊戲引擎設計中涉及到的一些面向對象設計的問題将會着重談到,其中包括命名規則、名域、運作時期的類型識别、運作時期的類型識别,單獨或者多重繼承、模闆(參數化的資料類型,泛型?)公共對象、引用計數、流處理、開始和關閉機制。
A. 1面向對象的軟體構造
發表于1988年的meyer的論文是一篇面向對象軟體工程的優秀參考,對堆棧、字元串、清單、隊列、映射、集合、樹、圖這些抽象資料類型的足夠深刻而廣泛的讀物是發表于1987年的Booch(估計是國外的一個論壇)
A.1.1軟體品質
軟體工程的目标是從使用者和程式員兩個角度上提高軟體的品質。期望的軟體品質表現在下面兩個方面
■ 從外部看:軟體夠快、可用性強、很容易使用。最終的使用者很看重這些品質。最終使用者包括将要使用代碼的小組成員,是以易用度是非常重要的。
■ 内部來講:軟體是可讀的,子產品化的,結構化的,程式員非常注重這些品質。
外部品質相對來講更重要,因為軟體構造的最終目标是面向客戶需要的。然而内部品質是提高外部品質的關鍵。面向對象的軟體設計是處理内部問題的,但最終對下面的外部問題将有很大的益處.
■ 正确性:軟體精确執行需求和規範所确定的任務的性能。
■ 健壯性:一個軟體處理各種包括非一般情況輸入的性能。
■ 可擴充性:那個軟體可以進行一定改變的性能。
■ 可複用性:一個程式可被新的軟體部分的或全部的重用的性能。
■ 相容性:軟體與其他軟體協作能的性能。
■ 效率:例如處理器、記憶體、外存儲器這些硬體資源的使用,實際是空間和時間的權衡。
■ 可移植性:軟體可被轉移到其他硬體或軟體平台上的能力。
■ 可檢驗性:軟體可被檢測代碼發想錯誤的性能。
■ 完整性:軟體系統保護它的不同部分免受各種非授權的讀寫,無論這種操作是有意還是無意的。
■ 易用度:學習使用的軟體的友善程度。學習内容包括執行程式、準備輸入資料、解釋輸出資料、挽救異常狀态。
軟體維護是一個修正目前代碼,加強其效能、擴充其代碼處理新的問題的能力,下面是一個代表性的軟體維護時間的分類清單(Meyer 1988):
n 由于使用者的需求改變占41。8%。毫無懸念,這是由于缺乏可擴充性。
n 資料格式上的改變占17。4%。無可置疑,這是由于初始設計沒有深入的研究資料的變化。
n 異常情況的定位:12。4%。
n 正常檢測占9。0%。因為特殊情況需要解決。軟體在正常情況下也要運作。
n 硬體改變(6。2%)與硬體有關的代碼的标準化能夠最小的改變這些封裝。這些标準化主要通過硬體相關代碼都粉狀進硬體驅動。
A.1.2子產品性
子產品是原子的、一緻的、強壯的、組織好了的包。這還不能夠真正定義子產品,但我們都清楚他到底是什麼。下面的規則将會幫助你确定哪一種軟體構造方式更子產品化。
n 可分解性:設計方法必需幫助吧一個問題分解成若幹個子問題,每個子問題獨立解決。
Ø 示例:自頂向下設計
Ø 反例:初始子產品
n 組合性:這種設計方法支援軟體産品的元素可以任意組合構成新的産品。
Ø 示例:數學函數庫
Ø 反例:組合的GUI和資料庫函數庫。
n 可了解性:子產品可以被很容易的被人閱讀和重組成新子產品的設計方法。
Ø 示例:一個數學函數庫有許多的導出功能,但是沒有其他的導入連接配接檔案。
Ø 範例:序列化的非獨立子產品,子產品A依靠子產品B,子產品B依靠子產品C,子產品C需要。。。。。
n 可持續性。一個确定問題的小的改表僅僅導緻一個子產品的改變,改變不應影響軟軟體的體系結構。
Ø 示例:符号常量(沒有代碼号),統一索引的原則。
Ø 反例:因為資料标示以後會變,是以導緻不能夠對使用者隐藏這些資料标示。
n 可保護性:這種設計方法産生這樣一種結構。這種結構的作用是使程式在反常情況下僅僅在一兩個子產品類發生。
Ø 例如:輸入和輸出的确認,就是抽象資料類型裡的預處理和後處理的概念。
Ø 反例:不規則異常,異常是由一個代碼子產品産生另一個代碼子產品處理,而另一個子產品可能是遠端的,但是這種機制違反了把反常情況限制的規則。
五條規則引出了五條用來保證子產品性的法則。每個法則所保證的規則在氣候的括号中指出。
n 語言化的子產品單元:子產品就像語言的中句子的成風一樣可組合。(分解性,組合性,可保護性)
n 少一點的接口,每一個子產品與外界的互動要盡可能的少(連續性、可保護新)
n 小接口,如果兩個子產品必需通信,他們的交換資訊必需盡可能的少,這叫做:低耦合(連續性,保護性)
n 清楚的接口:兩個子產品無論何時通信,都必須與子產品的内容清晰分開,這叫做直接耦合。(分解性,組合性,連續性,可了解性)
n 資訊隐藏:所有關于子產品的資訊必需私有化、除非這個資訊被宣布是公共的。
開放封閉的原則
這是一個子產品分解性上的最終要求,它意味着一個子產品必須同時又是封閉的又是開放的。
n 開放子產品:這個子產品是可擴充的,例如:它可以給現有的資料結構增加新的域或者對現有的資料結構添加新的操作。
n 封閉子產品:這個子產品是可被别的子產品使用的。這需要這個子產品又一個定義完美的穩定的接口,特别是在穩定方面,例如:這樣的子產品必需可以被編寫為一個函數庫。
咋一看:開放性和封閉性必然是相反的,如果一個子產品的公共接口保持一緻,但是内部是先生改變,這種子產品就可認為是既開放又封閉的。然而最好的修正是增加新的功能,繼承的概念是封閉和開放的最好實作方式。
A.1.3重用性規則
在軟體工程中,重用性是一個基本的問題,為什麼化那麼多的時間設計和代碼一個系統,而實際上,它有可能早已經在其他什麼地方存在了。這個問題并沒有一個簡單的答案。已經寫好的分類清單,處理堆棧或者其他基本的資料結構操作代碼是很容易找到的,但是這個問題中往往含有其他的因素,一些公司提供你所需要的庫,但是想要使用卻需要買一個授權,如果提供商的代碼有問題,你必須找他們自己來改,這也許是裡所不想的。
至少在你的本地環境裡,你可以努力使你自己的代碼子產品重用最大化,這兒有一些問題就是裡的資料子產品必須能夠産生可重用的單元。
n 資料類型的變化:子產品必需适用于各種類型的資料機構,模般或者參數化的資料類型可以幫你解決這個問題。
n 資料結構和機制上的變化:一套機制所進行的操作可能依賴于它的底層資料結構,子產品必須能夠處理不同的底層資料結構,重載可以幫你解決這個問題。
n 相關的正常操作,子產品必須有對處理底層資料結構正常操作的接口。
n 代理獨立性:一個子產品必須能夠使一個使用者使用其操作但無需知道它的内部實作機制和底層資料結構。例如:
x_is_in_table_t=search(x,t);
這是一個在t表裡尋找x的函數調用,它的傳回值是布爾型的。如果将要被查找得的表可能是多重類型的(例如:連結清單,樹,檔案等等),我們希望不要有下面這樣的大量的控制結構:
if ( t is of A)
apply search algorithm A
else if (t is of type B)
apply search a algorithm B
else if…..
無論在子產品還是在客戶代碼中,重載和多态可以解決這個問題。
n 子子產品的公共性,提取代碼重的公共部分是非常重要的!避免相似代碼的重複,因為在這種相似代碼塊中當一個小小的改變發生的時候,所有的相似代碼塊都要改變,這将會化很多時間來維護,而一個抽象的接口卻不暴露它内部的資料結構。
A.1.5函數(或過程)和資料
函數和資料,首先考慮那個那?回答這個問題的關鍵點在于軟體的可擴充性上,具體的講:就是連續性。在軟體的整個生命周期裡,因為系統的需求是規律性變化的,是以函數的變化相對較多。然而函數所操作的資料要保持一緻性,變化非常小。這是設計基于的對象的子產品的一種面向對象方法。
一個經典的設計模式就是自頂向下的設計方法。首先确定系統的抽象功能,然後逐級分解,使功能逐漸跟易于實作,這種方法邏輯性強,比較好組織。激勵了程式員。但是也有缺點,具體如下:
n 這種方法忽略了軟體要發展的本性。于是連續性上就出了問題,自頂向下的設計方法在短時間内提供了友善,但是一旦系統改變就得全盤重來,是以它留下了長期的災難。
n 軟體系統的概念由一個函數來實作這是不合适的。例如作業系統久久是不能夠由一個函數來實作的典型例子。真正的系統沒有頂。
n 這種設計方法不能提供任何可重用性,設計者一般都是基于現有要求分解功能,子系統僅僅能夠對應于現有系統,當系統變化的時候,子系統沒法用喽。
A.1.5面向對象的設計
面向對象的設計方式導緻了面向對象的軟體的體系結構的系統和子系統而不是函數。主要關鍵如下:
n 如何發現個對象。一個組織良好的軟體系統可以被看作是一個可操作的模型或是個整體的一部分。軟體的對象僅僅代表了一個真實世界對象。
n 如何描述一個對象。描述對象的标準方法是抽象的資料類型,一個抽象資料類型的定義包括類型(它是一個抽象資料類型的參數)、方法(對象所能提供的操作)、預處理(這個必須在所有操作之前進行)、後處理(所有操作之後執行)公共規則(方法的行為規則)。
面向對象的設計通常也将軟體系統的構造可以看作是抽象資料類型實作的結構化集合。仔細說就是:
n 基于對象的結構化子產品,系統是在資料結構的基礎上子產品化的。
n 資料抽象。對象可以描述為一個抽象資料類型的實作。
n 自動的存儲管理:無用的對象被底層語言系統自動的析構,而不需要程式員的幹涉。
類:每一個非簡單類型都是一個子產品,每一個進階的子產品都是一個類型,這種實作被視為是一類一子產品的範例
n 繼承:一個類看作定義為另一個的擴充或限制體。
n 參數化和動态綁定,程式實體可以涉及多個類,一個操作可以在不同的類中有不同的實作。
n 多重和多級繼承,可以給很多類定義一個繼承類,也可以給一個類定義多個繼承類。
一個語言是否支援上面這部分提到這些因素還是個問題。具體的講:smaltalk 和ada是具有以上所有因素的,然而他們功效不行,這本書中面向對象的代碼是c++的,但它卻不是一個完全面向對象的代碼,c++做範例非常好,在需要效率的地方提供很大的友善,一個公共的c++的謬誤就是它的效率卻無法和c比。記住一個編譯器是一個巨大的軟體系統,并且如同所有其他大型系統一樣也是敏感的,實作的往往都不好。目前這一代的c++編譯器可以産生緊湊而快速的代碼(參照Ellis和Stroustrup的c++參考書,1994)。Lippman的範例擴充集(1991)展示了c++的精彩特性。
A.2代碼風格,命名傳統與命名空間
軟體工程的一個目标是代碼必須是可讀的,在一個很多程式員分工合作的大型軟體開發環境裡,每一個程式員都想使用自己的一套代碼風格,這包括辨別符命名,空格的使用,對齊及前後縮放的樣式,花括号注疏的位置等等,如果一個小組的代碼又要供組内成員看,又要供組外客戶看,那麼我建議你最好使你們的代碼保持完全一直的風格。風格不夠一緻的代碼就與客戶的要求相脫離了,你的客戶是要了解你的代碼,然後把它用到他自己的程式中去。從管理上強制規定的代碼風格是一種選擇,但是要考慮到大家意見的紛争。目前很多的c++程式員都是現學c的,在那個時候,他們代碼風格就固定了。但是那其中很多的習慣卻與面向對象的思想格格不入,這些程式員使用着他們最開始學習時學到的風格。
命名傳統是非常重要的,特别當一個代碼閱讀者想要從一個由多程式員寫的多檔案的代碼中搞出他所想要得東西的時候。本書的附帶CD光牒中附帶的代碼中使用了一種很有效的命名傳統,它可以使讀者很容易的區分成員名,本地變量名,全局變量名,包括它是否是靜态的,這樣一來你就很容易找到相應變量的定義和作用範圍,而且辨別符的名字中包括類型資訊。嵌入資訊不象微軟的匈牙利命名法那樣冗長。但是對于代碼的閱讀和了解來說:那些已經非常有效了。
作為一個遊戲引擎來說:就像其他大型的庫一樣,不可避免的要和其他團隊的軟體庫內建,是以就有可能導緻類名或全局變量名的沖突。有可能發生這樣的情況:你定義你自己的矩陣類叫:Matrix,但是其他人也可能定義這樣名稱的類,c++的命名空間可以解決這樣的問題,這種方法在多庫協作的過程中非常通用,就是在類名和全局變量前面加一個獨一無二的字首。命名空間的構造隐含地破壞了類名,自己手工選擇地字首更使這種破壞很具體。
随書附帶的代碼民命規則是這樣的:類名和全局變量名都以Mgc為字首。函數名都首字母大寫;如果多個單詞構成一個名字,每一個單詞的首字母大寫,例如:設定一個類代表字元串,一個取得其長度的成員函數就叫GetLength.函數的辨別符命名也使用相同的規則,但是有字首。非靜态的資料成員以m_為字首,靜态的資料成員以ms_為字首。那個m代表成員(member),s代表靜态的(static),一個靜态的本地變量以s_起頭,一個全局的變量以g_起頭,一個全局靜态變量以gs_起頭,變量的類型也被考慮在内,也是标示符的字首但是是跟在成員或全局變量的下劃線後面。表A,一個不同類型命名代碼規則的清單,一般的标示符命名不使用下劃線,除了有字首在它前面的情況下,類常量也是以大寫字母起頭,并且可能包括下劃線,各種編碼規則是可以結合使用的,如下例:
unsigned int* auiArray= new int[16];
void ReallocArray(int iQuantity,unsigned int*&rauiArray)
{
delete[]rauiArray;
rauiArray=new unsigned int[iQuantity];
}
short sValue;
short& rsValue-sValue;
short* psValue=&sValue;
class MgcSomeClass
{
public:
MgcSomeClass();
MgcSomeClass(const MgcSomeClass& rkObject);
protected:
enum{NOTHING,SOMETHING,SOMETHING_ELSE};
unsigned int m_eSomeFlag;
typedef enum{ZERO,ONE,TWO}Counter;
Counter m_eCounter;
};
下表中沒有列到的命名規則可以由原檔案推斷得出。
|
A.3運作時動态資訊
多态性提供了函數功能的抽象,一個多态的函數不用考慮調用對象的真實類型,但是你需要知道參數化對象的類型,或者你需要決定是否這個類型源于一個确定的類型,例如同一個雞肋指針指向一個子類,這個過程叫做動态類型映射,實時類型資訊提供了一種在程式運作時确定這種資訊的方法。
A.3.1單繼承層次的系統
一個單繼承的面向對象的系統有一個直接的樹構成,在這個樹裡面,節點代表類,邊代表繼承關系,假設節點V0代表類C0,節點V1代表類C1,如果C1是C0的子類,那麼在V0和V1之間就有一條邊,代表C1、C0兩者之間的繼承關系,這種直接的邊表示了父子類之間is-a的關系,圖A.1展示了一個簡單的單繼承的層次關系。
樹的根是多邊形,長方形是一個多邊形,正方形是一個長方形,也是一個多邊形,三角形是一個多邊形,等邊三角形是一個三角形,非等邊三角形也是一個多邊形,但是正方形不是一個三角形,非等邊三角形也不是一個等邊三角形。
?????????????????圖a.1????????????????
一個運作時資訊系統是一個這種樹的實作,基本的運作時資訊資料類型存儲着一個程式在運作時可能需要的類型判定資訊并且存儲着一個基類的連接配接,以備程式确定一個類是否繼承于另一個類,這種最簡單的代表不存儲類的類的資訊,僅僅有一個對基類的連接配接,然而,這的确是非常有用的,雖然僅僅存儲一個字元串名字,實際上,這個字元串将會被用在以後被描述到的流式系統中,這個字元串在以後DEBUG的過程中用于快速确定類的類型是非常有用的。
class MgcRTTI
{
public:
MgcRTTI (const char* acName, const MgcRTTI* pkBaseRTTI)
:
m_kName(acName)
{
m_pkBaseRTTI = pkBaseRTTI;
}
const MgcRTTI* GetBaseRTTI () const { return m_pkBaseRTTI; }
const MgcString& GetName () const { return m_kName; }
private:
const MgcRTTI* m_pkBaseRTTI;
const MgcString m_kName;
};
在一個繼承層次樹中的基類MgcObject必須包含有對RTTI系統的基本支援,其最小的機構如下:
class MgcObject
{
public:
static const MgcRTTI ms_kRTTI;
vritual const MgcRTTI* GetRTTI()const
{
return &ms_kRTTI;
}
bool IsExactlyClass(const MgcRTTI*pkQueryRTTI)const
{
return(GetRTTI()==pkQueryRTTI);
}
bool IsExactlyClass(const MgcRTTI*pkQueryRTTI)const
{
return(GetRTTI()==pkQueryRTTI);
}
bool IsDerivedFromClass(const MgcRTTI*pkQueryRTTI)const
{
const MgcRTTI*pkRTTI=GetRTTI();
while(pkRTTI)
{
if(pkRTTI==pkQueryRTTI)
return true;
pkRTTI=pkRTTI->GetBaseRTTI();
}
return flase;
}
void* DynamicCast(const MgcRTTI*pkQueryRTTI)
{
return(IsDerivedFromClass(pkQueryRTTI)?this:0);
}
};
在繼承樹結構中的每一個子類都有一個靜态的MgcRTTI并且其最小結構如下:
class MgcDerivedClass:public MgcBaseClass
{
public:
static const MgcRTTI ms_kRTTI;
virtual const MgcRTTI*Get RTTI()const
{
return &ms_KRTTI;
}
};
無論MgcBaseClass在那兒,或者有什麼而繼承,注意:獨一無二的标示都是可以的,因為靜态的MgcRTTI成員都有自己不同的運作時記憶體位址,是以衍生子類的原檔案必須含有:
const MgcRTTI MgcDerivedClass::ms_kRTTI("MgcDerivedClass",
&MgcBaseClass::ms_kRTTI);
A.3.2多繼承的層次系統
一個非繼承的面向對象的多繼承系統可由一個非循環的圖表構成,在這個圖表中節點代表類,便代表繼承關系,假設節點Vi代表類Ci,對于i=1,2,3,如果C2繼承于C1和C0,這樣V2就有兩條邊各自指向V1和V0來代表這種多繼承的關系。圖A.2展示了一個多繼承層次關系,一個多繼承前提下的運作時資訊系統就是一個這種直接非繼承圖示關系的實作。單繼承系統的運作時資訊系統有一個指向基類的指針,而多繼承系統則需要一個制向所有基類的指針清單。最簡單的實作不存儲類的資訊,僅僅存儲那個指向基類的指針,為了支援判定基類的數目,c風格的省略号在構造函數種被用到,是以需要标準的參數支援,對于大多數編譯器來說,要包括stdarg.h這個檔案提供操作參數解析的宏指令。
--------------------圖a.2---------------------------??????????????????
class MgcRTTI
{
public:
MgcRTTI(const char*acName,unsigned int uiNumBaseClasses.....);
m_kName(acName)
{
if(uiNumBaseClasses==0)
{
m_uiNumBaseClasses=0;
m_apkBaseRTTi=0;
}
else
{
m_uiNumBaseClasses=uiNumBaseClasses;
m_apkBaseRTTi=new const MgcRTTI*[uiNumBaseClasses];
va_list list;
va_start(list,uiNumBaseClasses);
for(unsigned int i=0;i<uiNumBaseClasses;i++)
m_apkBaseRTTI[i]=va_arg(list,const MgcRTTI*);
va_end(list);
}
}
~MgcRTTI()
{
delete[] m_apkBaseRTTI;
}
unsigned int GetNumBaseClasses()const
{
return m_uiNumBaseClasses;
}
const MgcRTTI* GetBaseRTTI(unsigned int uiIndex)const
{
return m_apkBaseRTTI[uiIndex];
}
private:
unsigned int m_uiNumBaseClasses;
const MgcRTTI** m_apkBaseRTTI;
const MgcString m_kName;
};
在單繼承層次系統樹種的根類提供了一種成員函數,這個成員函數用來搜尋被探測的樹來決定一個類和另一個類是否是同繼承關系,在多繼承層次樹中有一個技術問題就是可能會有不止一個節點沒有邊,也就是這種繼承會有多個根類,為了解決問題,會提供一個單個得根類,它的任務就是為這個繼承表中的任何系統提供接口。
多繼承系統中的根類器結構構造很像單繼承樹的根類結構,除了其中成員函數的實作,IsDerivedFromClass就是用來處理RTTI的基類指針清單
bol MgcObject::IsDerivedFromClass(const MgcRTTI* pkQueryRTTI)const
{
const MgcRTTI*pkRTTI=GetRTTI();
if(pkRTTI==pkQueryRTTI)
return true;
for(unsigned int i=0;i<pkRTTI->GetNumBaseClasses();i++)
{
if(IsDerivedFromClass(pkRTTI->GetBaseRTTI(i)))
return true;
}
return false;
}
基類仍舊提供相同的靜态成員RTTI函數一個操作其類位址的成員函數,例如,考慮一下下例:
class MgcDerived::public MgcBase0,MgcBasel
{
public:
static const MgcRTTI ms_kRTTI;
virtual const MgcRTTI* GetRTTI()const
{
return &ms_kRTTI;
}
};
MgcBase0和MgcBase1或者是MgcObject類的對象,或者由MgcObject衍生,衍生自這個類的源檔案必需含有
const MgcRTTI MgcDerived::ms_kRTTI("MgcDerived",2,&MgcBase::ms_kRTTI,&Mgc_kRTTI,&MgcBase1::ms_kRTTI);
A.3.3宏支援
程式中用到宏可以有效的解決代碼的冗長問題,下面這個宏可以提供給單繼承和多繼承系統。
macros in MgcRTTI.h
#define MgcDeclareRTTI/
public:/
static:/
static const MgcRTTI ms_kRTTTI;/
virtual const MgcRTTI*GetRTTI()cosnt {return &ms_kRTTI;}
#define MgcImplementRootRTTI(rootclassname)/
const MgcRTTI rootclassname::ms_kRTTI(#rootclassname,0)
macros in MgcObject.h and MgcObjectM.h
#define MgcisExactlyClass(classname,pObject)/
pObject?Pobject->IsExactlyClass(&classname::ms_kRTTI):flase)
#define MgcIsDerivedFromClass(classname,pObject)/
pObject?pObject->IsDerivedFromClass(&classname::ms_kRTTI):false)
#define MgcStaticCast(classname,pObject)/
((classname*)pObejct)
#define MgcDynamicCast(classname.pObject)/
pObject?(classname*)pObject->DynameicCast(&classname::ms_kRTTI):0)
宏MgcDeclareRTTI被放置在頭檔案的類聲明裡,注意:生命域是public,是以任何其它跟在這個宏調用後面的類聲明如果需要的話需要定義其他的生命域
下面這個是個提供給單繼承系統的宏:
#define MgcImplementRTTI(classname,baseclassname)/
const MgcRTTI classname::ms_kRTTI(#classname,&baseclassname::ms_kRTTI);
并且它必須在類定義的原檔案中使用,對于多繼承系統來講:這樣的宏是不可能的,因為c風格的宏不允許大量的參數。
A.4模闆
模闆,有時也叫做:參數化資料類型,用來在具有相同結構的類之間共享代碼。這方面典型的例子是對象的堆棧,對于一個有限的堆棧來講:對它的操作包括入棧(push)、出棧(pop)、判定是否為空(isempty)、判定是否已滿(isfull)以及讀棧頂元素(讀棧頂元素但不使其出棧),這些操作與堆棧存儲對象是什麼類型是沒有關系的,一個堆棧可以被實作用于存儲整數型和浮點型,各自使用樹組存儲堆棧元素,唯一的不同是整數的堆棧使用了一個整數樹組,而浮點的堆棧使用了一個浮點的樹組。模闆的使用可以使編譯器根據程式需求的類型産生對象代碼。
template <class T>calss Stack
{
public:
Stack(int iStackSize)
{
m_iStackSize=sStackSize;
m_iTop=-1;
m_akStack=new T[iStackSize];
}
~Stack(){delete[] m_akStack;}
bool Push(const T&rkElement)
{
if(m_iTop<m_iStackSize)
{
m_akStack[++m_iTOp]=rkElement;
return true;
}
return false;
}
bool Pop(T& rkElement)
{
if(m_iTop>=0)
{
rkElement=m_akStack[m_iTop--];
rreturn true;
}
return false;
}
bool GetTop(T& rkElement)const
{
if(m_iTop>=0)
{
rkElement=m_akStack[m_iTop];
return true;
}
return false;
}
bool IsEmpty()const{return m_iTop==-1;}
bool IsFull()const(return m_iTop==m_iStackSize-1;}
protected:
int m_iStsckSize;
int m_iTop;
T* m_akStack;
};
宏可以産生針對多類型的代碼,但是它對由此産生的副作用不敏感。盡管它也可以實作同時支援整形和浮點型兩種類型的堆棧,但是它會在代碼的維護方面産生問題,如果一個檔案改變,其他的檔案都會發生相關的變化,當那兒有大量的類型共享代碼的時候,這種問題還會加劇,模闆提供了一種講這些改變定位在一個檔案的方法。
模闆為堆棧,連結清單、數組這些抽象資料類型的容器類提供了一個不錯的選擇,标準的模闆庫可以內建到一個遊戲引擎中,當處理這種容器類對象的的時候一個問題時必須明确的,就是它必然會有一定的邊界效應,特别是當它構造和析構的時候。如果一個标準模闆庫類對象要改變它的大小,它會一次型申請一組存儲空間,把舊的容器類的内容複制進行的容器記憶體内,然後釋放原先的記憶體。這種機制有一個隐含的假設:底層資料是本地的。如果資料是由動态申請的(堆裡的)類對象,則這種記憶體複制會造成記憶體洩漏,隐含的邊界效應(side effect)就發生喽!,這對于下一節的共享對象和引用計數是非常明确的,如果标準模闆庫不支援這些邊界效應,則遊戲引擎需要實作自己的容器類型。
A.5共享對象和引用計數
在遊戲引擎當中,對象的共享是天生的。包含大量資料的模型共享以後以适應最少的記憶體,渲染狀态也可以共享,具體的講,例如:紋理圖檔在渲染體之間共享。在一個遊戲因輕重,如果靠人工(manully)管理共享對象必然會導緻遺漏一些對象(對象的洩漏)或者是析構了一些正在被使用的對象,是以一個更為自動的共享對象的管理是共享對象的定制管理,大多數流行的系統都是給根類對象增加一個引用計數器,一個對象的被另一個對象引用一次,它的引用計數器就加1,一旦引用計數器減至0,這個對象在系統類就不再被引用了,是以就要被删除了。
引用計數器的細節可以公開,以便于調整引用計數器的程式對其負責。這種機制給程式員正确的管理對象添加了極大的信心。另一個選擇是實作一個智能指針系統區在内部調整裡的引用計數器,以便在程式在需要特殊的處理時進行幹涉。是以對于程式員來講:管理對象的負擔被大大減輕了!
除了實時運作資訊以外,根類MgcObject現在包括了下面的代碼來支援引用計數:
public:
MgcObject(){m_uiReferences=0;ms_uiTotalObjects++;}
~MgcObject(){ms_uiTotalObjects--;}
void IncrementReferences(){m_uiReferences++;}
void DecrementReferences(){if(--m_uiReferences==0)delete this;}
unsigned int GetReferneces(){return m_uiReferences;}
statci unsigned int GetTotalObjects(){return ms_uiTotalObjects;}
private:
unsigned int m_uiReferences;
static unsigned int ms_uiTotalObjects;
靜态的計數器跟蹤對象現在在系統中的全部數目,程式執行的初始值是0.
智能指針系統在整個基礎上被實作,使用了模闆。
template <class T>
class MgcPointer
{
public:
// construction and destruction
MgcPointer (T* pkObject = 0);
MgcPointer (const MgcPointer& rkPointer);
~MgcPointer ();
// implicit conversions
operator T* () const;
T& operator* () const;
T* operator-> () const;
// assignment
MgcPointer& operator= (const MgcPointer& rkReference);
MgcPointer& operator= (T* pkObject);
// comparisons
bool operator== (T* pkObject) const;
bool operator!= (T* pkObject) const;
bool operator== (const MgcPointer& rkReference) const;
bool operator!= (const MgcPointer& rkReference) const;
protected:
// the shared object
T* m_pkObject;
};
//---------------------------------------------------------------------------
template <class T>
inline MgcPointer<T>::MgcPointer (T* pkObject)
{
m_pkObject = pkObject;
if ( m_pkObject )
m_pkObject->IncrementReferences();
}
//---------------------------------------------------------------------------
template <class T>
inline MgcPointer<T>::MgcPointer (const MgcPointer& rkPointer)
{
m_pkObject = rkPointer.m_pkObject;
if ( m_pkObject )
m_pkObject->IncrementReferences();
}
//---------------------------------------------------------------------------
template <class T>
inline MgcPointer<T>::~MgcPointer ()
{
if ( m_pkObject )
m_pkObject->DecrementReferences();
}
//---------------------------------------------------------------------------
template <class T>
inline MgcPointer<T>::operator T* () const
{
return m_pkObject;
}
//---------------------------------------------------------------------------
template <class T>
inline T& MgcPointer<T>::operator* () const
{
return *m_pkObject;
}
//---------------------------------------------------------------------------
template <class T>
inline T* MgcPointer<T>::operator-> () const
{
return m_pkObject;
}
//---------------------------------------------------------------------------
template <class T>
inline MgcPointer<T>& MgcPointer<T>::operator= (const MgcPointer& rkPointer)
{
if ( m_pkObject != rkPointer.m_pkObject )
{
if ( m_pkObject )
m_pkObject->DecrementReferences();
m_pkObject = rkPointer.m_pkObject;
if ( m_pkObject )
m_pkObject->IncrementReferences();
}
return *this;
}
//---------------------------------------------------------------------------
template <class T>
inline MgcPointer<T>& MgcPointer<T>::operator= (T* pkObject)
{
if ( m_pkObject != pkObject )
{
if ( m_pkObject )
m_pkObject->DecrementReferences();
m_pkObject = pkObject;
if ( m_pkObject )
m_pkObject->IncrementReferences();
}
return *this;
}
//---------------------------------------------------------------------------
template <class T>
inline bool MgcPointer<T>::operator== (T* pkObject) const
{
return ( m_pkObject == pkObject );
}
//---------------------------------------------------------------------------
template <class T>
inline bool MgcPointer<T>::operator!= (T* pkObject) const
{
return ( m_pkObject != pkObject );
}
//---------------------------------------------------------------------------
template <class T>
inline bool MgcPointer<T>::operator== (const MgcPointer& rkPointer) const
{
return ( m_pkObject == rkPointer.m_pkObject );
}
//---------------------------------------------------------------------------
template <class T>
inline bool MgcPointer<T>::operator!= (const MgcPointer& rkPointer) const
{
return ( m_pkObject != rkPointer.m_pkObject );
}
//---------------------------------------------------------------------------
指派操作符在在調整引用計數前必需比較指針所指的值,以防止違法操作。
MgcPointer<MgcObject>spPointer=newMgcObject;
spPointer=spPointer;
MgcObject的構造函數設定引用為0.MgcPointer的構造函數設定計數為1.如果初始對照在指派操作符裡不出現,那麼對DecrementReference的引用計數将會把引用計數減為0,那就删除了這個對象了。進而,指針rkPointer,M_pkObject都指向了一塊不屬于程式的記憶體,指派的結構就會使指針指向非法的記憶體塊,而IncrementReferences的調用則會寫向一個非法的記憶體塊,盡管這樣的錯誤在程式中不一定發生,但是這種情況可能會由于指針的混淆而加劇。
為了友善,MgcObject或者是其它由它繼承的類将會避免模闆符号的冗長,支援定義智能指針的宏是:
#define MgcSmartPointer(classname) /
class classname; /
typedef MgcPointer<classname> classname##Ptr
為了客戶代碼的友善,每一個類可以把這個定義放在它的頭檔案中,例如:MgcObject.h将會包括MgcObject和其狀态的類定義
MgcSmartPointer(MgcObject);
這就确定了MgcObjectPtr的類型。這種在宏中支援類名的向前定義,支援了智能指針類型的向前定義。
這兒可能會需要構造一個智能指針指向一個指針或者智能指針,例如:類MgcNode,這是場景圖型的内部代理,它繼承自MgcSpatial,頁節點代理了場景圖形,多态型允許指派:
MgcNode*pkNode=<some node in scen graph>;
MgcSpatial*PkObject=pkNode;
從理論上講:MgcNodePtr類型的智能指針繼承自MgcSpatialPtr,但是語言并不支援這一點,但是繼承發生的話,在智能指針類的内部隐含的操作符轉換允許這樣的附帶效應,例如:
//this code is valid.
MgcNodePtr spNode=<some node in secne graph>;
MgcSpatialPtr spObject=spNode;
//this code is not valid when class A is not derived from class B
MgcAPtr spAObject =new A;
MgcBptr spBObject =spAObject;
這種隐含的轉換也支援智能指針與空指針的對比、就像正常的指針那樣:
MgcNodePtr soNode=<some node in scene graph>;
MgcSpatialPtr spChild=spChild->GetChildAt(2);
if(spChild)
{
<do something with spChild>;
}
下面是一個簡單展示智能指針的使用和清空的例子。這個MgcNode類為它的子類存儲了一個智能指針的數組.
MgcNodePtr spNode=<some node in scene graph>;
MgcNode* pkNode=new MgcNode;//pkNode references=0
MgcNodePtr spChild=new MgcNode;//pkNode references=1
spNode->attchChild(spChild);//pkNode references=2
spNode->detachChild(spChild);//pkNode references=1
spChild=0;//pkNode references=0//desroy it
這展示了如何正确的結束一個智能指針的使用,在這段代碼中那個删除spChild的調用做得非常好。如果spChild指向的對象有一個正的引用計數,明确的調用了析構函數強迫性删除,另外一個指向相同對象的對象就有了個懸挂的指針,如果代替智能指針操作而是引用計數被減小,如果那兒有另外一個對象指着這個對象,所指的對象就不會被破壞,是以像下面這樣的代碼是安全的:
MgcNodePtr spNode=<some node in scene graph>;
MgcNode* pkNode new MgcNode;//pkNode references=0
MgcNodeptr spChild =new MgcNode;//pkNode references=1
spNode->AttachChild(spChild);//pkNode references=2
spChild=0//pkNode references=1,
//no destruction
注意:在這段代碼種沒有給智能指針指派為0的操作,這個智能指針的析構被調用,pkNode的引用計數仍舊被減少為0.
當使用智能指針的時候,下面這些另外規則也必須堅持。智能指針僅僅提供給動态對象的。不能指向對象堆棧。例如:
void MyFunction()
{
MgcNode kNode; //kNode references=0
MgcNodePtr spNode=&kNode; //kNode references=1
spNode=0; //kNode references=0
//kNode is deleted
}
這個操作注定要失敗,因為kNode在堆棧裡,在收回堆棧記憶體的時候隐含的删除了對象,對象不在堆裡面。
使用智能指針作為函數參數或是函數傳回值也有缺陷,下面的這個例子說明了這種危險:
void MyFunction(MgcNodePtr spNode)
{
<do nothing>;
}
MgcNode* pkNode=new MgcNode;
MyFunction(pkNode);
//pkNodeNow points to invalid memory
在給pknode指派記憶體時有0個引用,MyFunction這個函數調用通過類的複制構造函數在堆棧中增加了一個MgcNodePtr的執行個體,在從函數中傳回時,這個MgcNodePtr的執行個體被析構,在這個過程中,pkNode有0個引用,它也将要被破壞,是以下面的代碼才是安全的:
MgcNode*pkNode=new MgcNode;//PkNode references=0
MgcNodePtr spNode=pkNode;//pkNode references=1
MyFunction(spNode);//pkNode references increase to 2,
//then decrease to 1
//pkNode references=1 at this point
相關的問題如下:
MgcNodePtr MyFunction()
{
MgcNode pkReturnNode=new MgcNode;//references=0;
return pkReturnNode;
}
MgcNode* pkNode=MyFunction();
//pkNode now points to invalid memory
通過編譯器,作為函數傳回值的一個暫時的MgcNodePtr的一個執行個體隐含的生成了。因為拷貝構造函數生成這個執行個體,是以pkNode的引用計數是1,這種暫時的執行個體在下面不再需要,會被隐含的析構,pkNode的引用計數又變為0,是以它也被析構,下面的代碼将會是安全的。
MgcNodePtr spNode= myFunction();
//spNode.m_pkObject has one reference
這個暫時的執行個體增加了pkReturnNode的引用計數至1。拷貝構造函數又被用來制造一個spNode節點,是以引用計數被增加到2,是以當暫時對象被析構的時候,引用計數仍為1.
A.6流處理技術
遊戲引擎需要存儲的一緻性,遊戲的内容一般是由一個模組化工具生成必需導出一種遊戲程式能導入的格式,遊戲程式本身也需要存儲自己的資料以被一段時間之後重新讀出。資料的流式處理意味着在連個媒體之間映射資料的過程,典型的是在硬碟存儲器和記憶體之間,在這一部分我們将要讨論記憶體和硬碟之間的資料傳輸,但是這一種方法直接提供記憶體塊的傳輸(并支援通過網絡的資料傳輸)一個處理流的類便産生了---MgcStreaM.
A.6.1資料存儲
通常被存儲到硬碟上的資料是場景圖形資料。盡管可以在通路時周遊一個場景圖型并且存儲沒一個場景中物體,但是這樣就出現了兩方面的複雜化。一方面是:一些物體使可以被場景種不同的圖形所共享的。在這種情況下,一個物體有可能被存儲兩次。另一方面:物體可能含有指向您一個物體的指針。這樣情況主要發生在場景圖中節點的賦子關系的情況,詳細的說就是:一個存儲的場景圖型必須能夠重新讀進記憶體,并且這其中所有的物體關系也必須可讀。抽象的看這個問題:就是一個場景圖形就是一個物體(類型是MgcObject)的抽象圖(估計是指資料結構的圖,譯者按),圖中的節點代表物體,弧代表物體之間的指針關系。每一個物體又有抽象的成員,具體的講就是本地資料類型(整形、浮點型、字元串型等等)。這個圖必需也要被存儲在硬碟上,以便以後可以被修改。,這就意味着圖中的節點和弧必需以某種合理的形式存儲下來。而且每個圖隻能被存儲一次,存儲這樣的一個場景圖的過程與存與生成并存儲一個圖中對象連結清單到硬碟是等價的,并且對象之間所有的關系也要存儲下來。如果圖中有多連接配接的單元,那麼每一個單元也會被周遊存儲,對多個抽象對象存儲的支援是很容易實作的。類MgcStream能夠生成一個頂層的存儲對象的連結清單,典型情況是:這是一些場景圖形的根元素,但是也可能是其他對象的的需要存儲的狀态屬性。為了支援對這種檔案的讀,并且的到一個相同的頂級元素的連結清單,在每一個頂級元素所相關的抽象圖被寫進硬碟之前,一個資訊标記塊必需被寫入。一個簡單的方法就是寫進一個字元串。
标明所周遊的圖中的不相同對象的數目并且要把每個通路到的對象到要插進通路到的對象連結清單中,為了I/O的速度,一種理想的資料結構就是哈希表,把圖周遊一遍,依舊可以建立一個哈西表,通路哈西表就像通路連結清單一樣,然後再把對象的資料存進硬碟。為了支援讀入,對象的運作時類型資訊首先被寫入,為了支援檔案塊的快速讀寫,接着寫入的就是所存儲資料的byte數。盡管不是所有的資料都需要被寫入,但任何本地資料類型都要用标準c++的流操作符處理。對象成員不是本地資料類型的或者是不是MgcObject類的使用他們自己的流操作符處理寫入,一些資料成員使可以由其他資料成員導出的,那麼就意味着圖中的對象在成員之間存在相關性。這張圖中僅僅根項目需要被寫入,一旦重新讀入,相關的對象成員就可被恰當的構造。
一個對象指針資料成員可以被存儲為一個無符号整型記憶體位址,因為一個記憶體位址就是圖中一個弧的表示,然而,當存儲後的檔案再被讀入的時候,這個指針值就不再是合法的記憶體位址了,這個問題的處理方法在下一節裡讨論。
A.6.2資料的讀入
讀入的過程比寫的過程更複雜。因為原先在硬碟上存儲的指針值是非法的,每一個對象首先必須在記憶體中建立,然後天入從硬碟中讀出的資料,然後對象之間例如父子關系這樣的連接配接關系必須被建立。不考慮硬碟的指針值的非法性,然後關于圖的運作時類型資訊就要被讀入,每一個對象的位址。與一個硬碟中的指針指相對應,是以那個存儲不同對象的哈西表又可以被用來跟蹤硬碟指針值(叫做鍊結id)和對象的動态記憶體位址之間的相關性。當所有的對象都在記憶體中間裡的時候,這個哈西表就完成了,這個哈西表可以像連結清單一樣疊代通路,每一個對象的鍊結id将要被動态記憶體位址所取代.這也是鍊結編譯器生成的obj檔案的原理。
下面是從一個流裡面讀出一個對象的步驟:動态類型資訊首先被讀入,以便于确定對象的類型。然後是檔案塊的大小既byte數目被讀出。建立一個對象所需要的全部資訊現在就知道了。一個合适的構造函數和任何設定方法被調用來創造這個被讀入的對象,僅僅依靠運作時類型資訊來确定使用那個構造函數是不夠的。是以,每個類要提供一個靜态的工廠模式函數,然後MgcStream對象保持一個這種函數的哈西表;哈西表的鍵是運作時類型資訊,那個工廠模式的函數則充當一個構造器,利用已經讀入的對象的記憶體塊,生成一個正确類型的對象,并利用那個記憶體塊裡面的資訊初始化這個對象。
A.6.3流技術支援:
MgcStream在高層支援下面這些接口:
class MgcStream
{
public:
// construction and destruction
MgcStream ();
~MgcStream ();
// The objects to process, each object representing an entry into a
// connected component of the abstract graph.
bool Insert (MgcObject* pkTopLevel);
void RemoveAll ();
unsigned int GetObjectCount ();
MgcObject* GetObjectAt (unsigned int uiIndex) const;
bool IsTopLevel (MgcObject* pkObject);
// file loads and saves
bool Load (const char* acFilename);
bool Save (const char* acFilename);
// memory loads and saves
bool Load (char* acBuffer, int iSize);
bool Save (char*& racBuffer, int& riSize);
// linking support
class Link
{
public:
Link (MgcObject* pkObject);
}
};
MgcTStorage類代表了一個模闆化大小可變得數組存儲類。在一個程式中,如果要存儲一個檔案到硬碟需要:
MgcStream kStream;
for(each pkObject worth saving)
kStream.Insert(pkObject);
kStream.Save("myfile.mff");
kStream.removeAllObjects();
在一個程式中讀入一個檔案到記憶體如下:
Stream kStream;
kStream.Load("myfile.mff");
for(unsigned int uiIndex=0;uiIndex<kStream.GetObjectCount();uiIndex++)
MgcObject*pkObject=kStream.GetObjectAt(uiIndex);
<application-specific handling of the object goes here>;
kStream.remove AllObjects();
這個RemoveAllObjects()函數清空了流對象,以備于以後出存或讀入對象。基類MgcObject提供了一個堆相對于自身的流支援。
public:
//support for loading
static MgcObject*Factory(MgcStram&rkStream);
virtual void Load(MgcStream& rkStream,MgcStream::Link*pkLink);
virtual void Link(MgcStream& rkStream, MgcStream::Link*pkLInk);
//'support for saving
virtual Register(MgcStream&rkStream);
virtual void Save(MgcStream&rkStream);
流的Save()函數疊代所有的頂級對象,并且調用每一個對象的注冊方法。這個過程的第一步是:周遊抽象圖及把圖中不同的對象添加進由流所維護的哈西表中,便曆之後,流對象疊代通路哈西表,然後調用每一個對象的Save()方法。
流的Load()方法讀檔案,然後通過讀運作是類型資訊和塊大小一次讀一個對象。靜态的工廠模式函數通過hash表被查找到然後被調用。然後這個工廠模式函數創造一個對象然後調用它的load()函數,對象指針和鍊結id都在流對象的的哈西表裡,所有的對象都讀入以後,通過哈西表的疊代器調用每一個函數的link函數進而用記憶體位址代替鍊結id。在這個過程中任何頂級對象都被放在流對象的這種對象地連結清單中,以便于程式操作他們。
需要處理的一個麻煩是: 鍊結id在對象被讀入和對象在鍊結兩個時刻的一緻性。咋一看,鍊結id可以存儲在對象的成員MgcObject指針裡,但是這種方法當共享對象和智能指針出現的時候就不行喽。如果一個成員有一個智能指針的成員,則鍊結id的指派操作則隐含地強制調用了引用計數的操作,因為鍊結id不是一個合法的記憶體位址,任何對其成員函數的調用都是不合法的并且将會出錯,是以鍊結id必需被獨立地存儲為一個規則指針。MgcStream類定義了一個嵌套的類來支援這種帶有一個(用來對目前所處理的對象進行跟蹤的)索引的MgcObject鍊結,當一個對象的工廠模式函數被調用的時侯,MgcStream::Link制造一個數組,并且傳遞給Load函數,任何鍊結ID都在這個數組中存儲,當基類的lOAD函數被調用的時候,鍊結ID就和流對象裡哈西表你的對象聯系在類一起。當LINK()程式段運作的時候,鍊結ID被船體給所有的LINK函數被用作在尋找它自身的替代品--動态記憶體位址。
下面這段僞代碼講要說明流是怎樣實作這個功能的,首先假設MgcDerived繼承自MgcBase.
MgcObjejct *MgcDerived::Factory(MgcStream rkStream)
{
MgcDerived* pkDerived=new MgcDerived;
MgcStream::Link*pLink=newMgcStreamLink;
pkDerived->Load(rkStream,pLink);
return pkDerived;
}
void MgcDerived::Load(MgcStream&rkStream,MgcStream::Link*plLink)
{
MgcBase::Load(rkStream,pkLink);
//load the mmeber data for 'this 'here.any MgcObject*member arre loaded into plLink.M_tObject for use as link IDs.
}
void MgcDerived::Link(MgcStream&rkStream,MgcStream::Link*pkLink)
{
MgcBase::Link(rkStrem,pkLink);
//Link he MgcObject*member for this 'here',this is
//generally the complicated part of the process since link resolution could require calling
//member function of 'this'to establish the connections between the loaded objects and'this'
bool MgcDerived::Register(MgcStream& rkStream)
{
if(!MGcBase::Register(rkStream)
{
//'this'si shared and was alrady registered by another owner
return false;
}
for each MgcObject pointer 'member'of 'this' do member. Register(rkStream);
}
void MgcDerived::Save(MgcStreamrkStream)
{
MgcBase::Save(rkStream);
//save the member data for 'this'here. any MgcObject*members
//have their pointer values written.The values are used as
//link IDs when the file si loaded at later date
A.7打開與關閉
系統中的很多類都需要在程式主函數開始前初始化并且在程式主函數結束以後有一定的終止操作。例如:一個矩陣類就可能存貯一個靜态常量作為它的初始矩陣,下面的代碼講可以使靜态資料成員在主函數之前初始化。
//in matrix.h
class Matrix
{
public:
Matrix(float fM00.float fM01, float fM02,
float fM10.float fM11, float fM12,
float fM20.float fM21, float fM22)
{
//intiallization of m_aafM[][]goes here
}
static const Matrix IDENTITY;
protected:
float m_aafM[3][3];
};
//in matrix.cpp
#include"matrix.h"
const Matrix Matrix::IDENTITY(1,0,0,0,1,0,0,0,1);
編譯器将會生成在主函數之前執行的Matrix構造函數以此來保證等一矩陣時在程式之前就準備好了的,如果在程式主函數之前需要初始化動态記憶體,那麼在主程式結束之後還得釋放,下面的代碼說明了c++實作這個的一種自動方式。這種機制也可以被用來任何靜态資料。
//in point.h
class Point
{
public:
Point(float fx,float fy,float fz);
{
m_fx=fx;m_fy=fy;m_fz=fz;
}
static void Intialize()
{
ms_uiQuantity=DEFAULT_QUANTITY;
ms_akHandyBuffer=new Point[ms_uiQuantity];
ZERO.m_fx=0;
ZERO.m_fy=0;
ZERO.M_FZ=0;
}
static void Terminate()
{
delete[]ms_akHandyBUffer;
}
static const point ZERO;
protected:
float m_fx,m_fy,m_fz;
enum{DEFAULT_QUANTITY=32};
static Point*ms_akHandyBuffer;
friend class _PointInitTerm;
};
//inpoint.cpp
#include"point.h"
const Point Point::ZERO;//just declare storage, no initialization
class _PointInitTerm
{
public:
_pointInitTerm(){Point::Initialize();}
~_PointIintTerm(){Point::Terminate();}
};
static_pointerInitTerm _forceInitTerm;
編譯器将會在主函數之前為_forceInitTerm生成一個構造函數調用,而在主函數之後生成一個解析函數調用。
這種開始和關閉的機制是自動的,但是有一個類之間的相關性需要補救,例如:假設類A有一個靜态的成員在主函數之前被初始化,類B有一成員必需被初始化為來自類A的一個數值,則初始化代碼則在那個類的源檔案中。編譯器同時處理兩個源檔案,但是生成的主程式前的代碼不支援任何具體的指令。
//in A.h
class A
{
public:
static void Initialize(){<initialize OBJECT here>;}
static void Terminnate(){<any cleanup goes here>;}
static A OEBJECT;
private:
friend class _AInitTerm;
};
//in A.cpp
#inlude "A.h"
A A::OBJECT;
class _AInitTerm
{
public:
_AInitTerm(){A::Initialize();}
~_AInitTerm(){A::Terminate();}
};
static _AInitTerm _forceInitTerm;
//in b.h
#include"A.h"
class B
{
public:
static void Initialize(){DEPENDENT_OBJECT=A::OBJECT;}
static void Terminate(){<any cleanup goes here>;}
static A DEPENDENT _OBJECT;
private:
friend class_BInitTerm;
};
//in B.cpp
#include "B.h"
A B::DEPENDENT_OBJECT;
class _BInitTerm
{
public:
_BinitTerm(){B::Initialize();}
~_BIinitTerm(){B::Terminate();}
};
static _BInitTerm _forceInitTerm;
在主程式之前:如果A::OBJECT的初始化在B::DEPENNDENT_OBJECT初始化之前被調用則不會出問題,然而如果順序相反,則B::DEPENDENT_OBJECT将會使用為A::OBJECT準備好的記憶體空間中并且将會是裡即可存取的空間,因為這個對象是靜态的。
這種類之間的相關性問題可以用定位的方式解決,這種方式使需要預處理的初始化操作調用它自身正确調用所需要的初始化操作,但是這個方法又導緻另一種問題:一個類的預處理初始化操作隻能被調用一次。解決方案是加入一個BOOL辨別符來标明那個初始操作是否已發生,如果它第二次被調用,就直接傳回。例示如下:
//in A.h
class A
{
public:
static void Initialize()
{
static s_bInitialized=false;
if(s_bInitialized)return;
s_bInitialized=true;
<initialize OBJECT here>;
}
static void Terminate(){<any cleanup goes here>;}
static A OBJECT;
private:
friend class _AInitTerm;
};