Nesty框架提供了在C下進行面向對象編程的技術,下面將以一些簡短的例子來說明其如何工作。
從第一個簡單的例子開始
NOBJECT是NOOC(Nesty Object-Oriented C)框架中所有面向對象類型的基類,NOBJECT作爲面向對象的編程接口提供了,對象拷貝,對象比較,對象哈希,對象字符串化,以及運行時類型識別和安全向下類型轉換等功能。因此所有NOOC都必須從NOBJECT派生, 以下例子定義了一個派生自NOBJECT的對象MY_CLASS:
NOBJECT_PRED(MY_CLASS);
NOBJECT_DEC(MY_CLASS, NOBJECT);
struct tagMY_CLASS
{
NOBJECT_BASE(NOBJECT);
};
NOBJECT_IMP(MY_CLASS, NOBJECT);
其中使用到了4個關鍵的宏:NOBJECT_PRED用於前置聲明對象;NOBJECT_DEC用於實際聲明對象,指明對象之間的繼承關係,從代碼例子中可以看出,MY_CLASS繼承自NOBJECT;由於對象實際上是用C的數據結構來模擬的,因此必須同時聲明一個同名的數據結構用於存儲對象的數據區,並tag作爲前綴,如例子中的tagMY_CLASS;由於在繼承關係中,子類需要包含父類的數據,因此還需要在數據結構的最前端使用NOBJECT_BASE宏類聲明父類的數據區,例子中的則聲明瞭父類NOBJECT的數據區。以上部分是對NOBJECT的聲明,通常位於頭文件中。
除了聲明NOBJECT之外,還需要通過宏NOBJECT_IMP來給對象提供實現,NOBJECT_IMP置於源文件中,否則編譯連接時將引發函數未提供實現的錯誤。NOBJECT_IMP同NOBJECT_DEC匹配,其指明的對象繼承關係要一直,例如指明其父類爲NOBJECT。
一旦對象成功聲明,便可以創建和使用對象,如下列代碼所示:
// 創建對象
MY_CLASS Obj = NNEW(MY_CLASS);
// 驗證其類型
NASSERT(NObjectClass(Obj) == NCLASSOF(MY_CLASS));
// 驗證繼承關係
NASSERT(NISA(Obj, NOBJECT));
// 釋放對象
NRELEASE(Obj);
由於對象是通過NEW產生的,在C語言無法提供自動垃圾回收,因此用戶使用完對象需要調用統一的接口NRELEASE來釋放對象,正如上例所示。
創建繼承關係的類
NOBJECT_PRED(MY_SUB);
NOBJECT_DEC(MY_SUB, MY_CLASS);
struct tagMY_SUB
{
NOBJECT_BASE(MY_CLASS);
};
NOBJECT_IMP(MY_SUB, MY_CLASS);
其語法與之前的一模一樣的,當成功聲明瞭MY_SUB後,便可以開始使用,如下所示:
MY_CLASS ObjSub = (MY_CLASS)NNEW(MY_SUB);
// 驗證繼承關係
NASSERT(NISA(ObjSub, MY_SUB));
// 安全向下類型轉換
MY_SUB ObjSub_2 = NCAST(ObjSub, MY_SUB);
NASSERT(ObjSub_2 != NULL);
在本例中,注意ObjSub是MY_CLASS類型的指針的,而實際上卻是指向了一個MY_SUB類型的實例,在NOOC的規則中,向上轉換必須使用強制類型轉換。而後值得關注的是,我們還需要將ObjSub向下轉換爲MY_SUB類型,以方便操作MY_SUB的數據和接口。在面向對象的規則中,基類(父類)通常作爲程序的接口來使用,因此基類的指針可以指向任何一個派生自基類的實例,就像上一個例子,ObjSub可以指向任何派生自MY_CLASS的類的實例,爲了進行有效的向下轉換,需要調用NCAST方法來實現動態轉換,NCAST會驗證指針實例的類層級關係,如上例中,只有ObjSub只MY_SUB的一個實例時,纔會返回有效指針,否則返回空指針。
如同上例一樣,使用完畢後還要釋放對象:
// 釋放對象
NRELEASE(ObjSub);
創建帶接口和數據的類
// xxx.h
NOBJECT_PRED(MY_CLASS);
NOBJECT_DEC(MY_CLASS, NOBJECT,
NINT (*GetValue)(MY_CLASS InObj);
);
struct tagMY_CLASS
{
NOBJECT_BASE(NOBJECT);
NINT Value;
};
NINLINE NINT MyClassGetValue(MY_CLASS InObj) { return NOBJECT_VCALL(InObj, MY_CLASS, GetValue)(InObj); }
// xxx.c
void MyClassCtor(MY_CLASS InObj) {
InObj->Value = 0;
}
NINT __MyClassGetValue__(MY_CLASS InObj) {
return InObj->Value;
}
NOBJECT_IMP(MY_CLASS, NOBJECT,
NCTOR_BIND(MyClassCtor)
NVCALL_BIND(MY_CLASS, GetValue, __MyClassGetValue__)
);
注意上例中的註釋,其指明兩部分代碼是分別位於頭文件和源文件中的,在上例中,在NOBECT_DEC的聲明最後插入了一個函數指針定義GetVallue,GetValue將作爲MY_CLASS的一個虛擬接口;注意在上一節的聲明中NOBJECT_DEC只描述了繼承關係,而本節中新增加了函數的聲明,這使用到了C99的宏可變參數的特性。同樣,上例中在tagMY_CLASS的聲明中新加了數據成員Value。
MY_CLASS Obj = NNEW(MY_CLASS);
NASSERT(Obj->Value == 5);
上例驗證調用NNEW創建MY_CLASS將觸發MY_CLASS的構造函數,將Value 初始化爲5。接下來我們通過NOBJECT_VCALL宏來調用MY_CLASS定義的虛擬接口。
NINT Val = NOBJECT_VCALL(Obj, NOBJECT, GetValue)(Obj);
NASSERT(Val == 5);
NOBJECT_VCALL包含了兩部分(注意觀察圓括號),第一部分NOBJECT_VCALL(Obj, NOBJECT, GetValue)用於獲取虛擬函數的地址。在面向對象類型的實例中,實例綁定了與其相關的類的類型(類類型),因此需要傳遞Obj實例來獲取Obj類類型信息(即NCLASS)。有了類類型,接着要獲取虛表,第二個參數傳遞MY_CLASS即告訴NOBJECT_VCALL,我要定位到MY_CLASS的虛表,最後一個參數GetValue及告訴我需要調用當前虛表中的哪個函數。當成功放回了虛擬函數的指針後,還要通過該函數指針調用實際的函數,從NINT (*GetValue)(MY_CLASS InObj);的定義,我們知道GetValue接受一個MY_CLASS實例作爲參數。如果你發覺上面的例子比較難以看懂,則下面例子分解出來的步驟:
typedef NINT (*NPfnMyClassGetValue)(MY_CLASS InObj);
NPfnMyClassGetValue FnGetValue = NOBJECT_VCALL(Obj, MY_CLASS, GetValue);
NINT Val = FnGetValue(Obj);
實際上NOBJECT_VCALL是一個很難用的接口(我很希望能把它設計得更簡單些),但爲了方便調用,你可以使用宏或者內聯(C99)去對NOBJECT_VCALL進行一次性封裝便可,很方便,不困難。例如上例中,我們可以給MY_CLASS定義一個虛擬調用的接口,如:
// 用內聯 需要C99支持
NINLINE NINT MyClassGetValue(MY_CLASS InObj) { return NOBJECT_VCALL(InObj, MY_CLASS, GetValue)(InObj); }
// 或者用宏包裝 結果取決於你如何使用
#define MyClassGetValue(InObj) NOBJECT_VCALL(InObj, MY_CLASS, GetValue)(InObj)
經過封裝後,對虛擬函數的調用將簡化爲下例所示:
NINT Val = MyClassGetValue(Obj);
NASSERT(Val == 5);
覆蓋虛擬接口
NOBJECT_PRED(MY_SUB);
NOBJECT_DEC(MY_SUB, MY_CLASS);
struct tagMY_SUB
{
NOBJECT_BASE(MY_CLASS);
NINT SubValue;
};
void MySubCtor(MY_SUB InObj) {
InObj->SubValue = 2;
}
NINT __MySubGetValue__(MY_CLASS InObj) {
MY_SUB Obj = (MY_SUB)InObj;
return Obj->SubValue;
}
NOBJECT_IMP(MY_SUB, MY_CLASS,
NCTOR_BIND(MySubCtor)
NVCALL_BIND(MY_CLASS, GetValue, __MySubGetValue__)
);
所使用的語法與之前是一模一樣的,不再介紹。唯一值得注意的是,在MY_SUB的NOBJECT_IMP實現中,將GetValue重新進行了綁定。因此當你通過調用NNEW來創建MY_SUB對象,並調用GetValue,這時執行的是__MySubGetValue__,而不是__MyClassGetValue__。到目前爲止,這便是NOOC中最爲重要的技術。
MY_CLASS ObjSub = (MY_CLASS)NNEW(MY_SUB);
// 訪問MY_CLASS.Value
NASSERT(ObjSub->Value == 5);
MY_SUB ObjSub_2 = NCAST(ObjSub, MY_SUB);
// 訪問MY_SUB.SubValue
NASSERT(ObjSub_2->SubValue == 2);