C語言下的面向對象編程技術

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來釋放對象,正如上例所示。


創建繼承關係的類

下例創建了一個繼承自MY_CLASS的子類MY_SUB:

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。

現在轉到源文件,MyClassCtor將作爲MY_CLASS的構造函數,在調用NNEW創建MY_CLASS對象的時候將被調用,該函數需要通過NCTOR_BIND宏來綁定到MY_CLASS的類實現中的,如上例所示。  

__MyClassGetValue__作爲虛擬接口GetValue的默認實現通過NVCALL_BIND綁定到了類實現中。NVCALL_BIND需要知道這個接口是哪個類型中定義,以便調用虛擬函數時先獲取該類型的虛表,因此要往NVCALL_BIND先傳遞MY_CLASS作爲參數,因爲GetValue是在MY_CLASS中定義的。其次,需要告訴NVCALL_BIND當前要綁定的是哪個接口,因此需要傳遞GetValue,最後需要傳遞GetValue 的實現__MyClassGetValue__。 由於__MyClassGetValue__僅僅是一個實現,從多態的特性中我們都知道,虛擬接口是可以被覆蓋(重寫)的,因此__MyClassGetValue__不應該通過頭文件暴露給用戶,在定義中添加雙下劃線__表明這是一個“受保護的”的方法。

現在我們通過實際例子使用MY_CLASS:
	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);

覆蓋虛擬接口

接下來將實現面向對象的終極武器,即運行時綁定(或多態)。多態機制提供了覆蓋虛擬函數的功能,這是面向對象的核心,可以令到不同類型的對象擁有不同的行爲,這恰好模擬了現實中多數同類事物在個別方面具備不同性質的事實。因此下面的例子將在MY_SUB的定義覆蓋MY_CLASS的虛擬接口,使其執行的是MY_SUB定義的行爲。
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_SUB所定義的構造函數MySubCtor將SubValue初始化爲2,但MY_SUB的基類MY_CLASS也定義了相應的構造函數,當調用NNEW創建MY_SUB的實例時,MY_CLASS和MY_SUB的構造函數將分別被調用,調用順序和C++構造函數的順序是一樣的。爲了驗證我的說法,請看下面的例子:
	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);

結束語

至此爲止,已經將NOOC最爲核心的功能介紹完畢,或許對於大多數人來說,這些例子都不足以說明NOOC的威力,爲此我在測試工程編寫了兩個單元測試:test_shooter_game和test_object_shooter_game(請查看test_shooter_game.cpp和test_object_shooter_game.cpp)。前者使用了C++語言來開發一個射擊遊戲的框架,其中包含了實際情形下用到各種C++的面嚮對象的技術,然後在test_object_shooter_game我使用NOOC重新實現整個框架,事實上證明這兩者完全吻合。不過NOOC也有其不足之處,例如接口不夠人性化,我不得不承認這點,但在C語言如此之多的限制之下,這已經是我能開發出來的最爲簡單的模式,如果您有更好的建議,不妨聯繫我。OOC即面向對象的C語言編程是一個非常有趣的話題,曾經有個國外作者著過一本名爲《Object-Oriented Programming With ANSI-C》書,有興趣的朋友可以去參考下,另外如果你在sourceforge.net下搜索OOC,也會出現一大堆的開源項目,有興趣的朋友大可以對比其他實現和NOOC實現之間的區別。關於Nesty的下載站點,請前往點擊打開鏈接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章