OOC 面向物件 C 語言程式設計實踐
面向物件是一種程式設計思想,雖然C並沒有提供面向物件的語法糖,但仍然可以用面向物件的思維來抽象和使用。這裡分享一套C面向物件的寫法,可以完成面向物件程式設計並進行流暢的抽象。這套寫法是在實踐中不斷調整的結果,目前已經比較穩定,進行了大量的功能編寫。
這套OOC有以下特性:
(1)沒有強行去模仿c++的語法設定,而是使用C的特性去面向物件設計
(2)實現繼承,組合,封裝,多型的特性
(3)一套命名規範去增加程式碼的可讀性
第一,封裝
在C中可以用struct來封裝資料,如果是方法,我們就需要用函式指標存放到struct裡面來模擬。
typedef struct Drawable Drawable; struct Drawable { float positionX; float positionY; };
typedef struct { Drawable* (*Create) (); void (*Init) (Drawable* outDrawable); } _ADrawable_; extern _ADrawable_ ADrawable[1];
static void InitDrawable(Drawable* drawable) { drawable->positionX = 0.0f; drawable->positionY = 0.0f; } static Drawable* Create() { Drawable* drawable = (Drawable*) malloc(sizeof(Drawable)); InitDrawable(drawable); return drawable; } static void Init(Drawable* outDrawable) { InitDrawable(outDrawable); } _ADrawable_ ADrawable[1] = { Create, Init, };
資料我們封裝在Drawable結構裡,通過Create可以再堆上建立需要自己free,Init是在棧上建立
函式封裝在ADrawable這個全域性單例物件裡,由於沒有this指標,所有方法第一個引數需要傳入操作物件
Create和Init方法將會管理,物件的資料初始化工作。如果物件含有其它物件,就需要呼叫其Create或Init方法
第二,繼承和組合
typedef struct Drawable Drawable; struct Drawable { Drawable* parent; Color color[1]; };
繼承,就是在結構體裡,嵌入另一個結構體。這樣malloc一次就得到全部的記憶體空間,釋放也就一次。嵌入的結構體就是父類,子類擁有父類全部的資料空間內容。
組合,就是在結構體,存放另一個結構體的指標。這樣建立結構體時候,要需要呼叫父類的Create方法去生成父類空間,釋放的時候也需要額外釋放父類空間。
這裡parent就是組合,color就是繼承。
繼承是一種強耦合,無論如何子類擁有父類全部的資訊。
組合是一種低耦合,如果不初始化,子類只是存放了一個空指標來佔位關聯。
可以看到,C裡面一個結構體可以,繼承任意多個父類,也可以組合任意多個父類。
color[1] 使用陣列形式,可以直接把color當做指標使用
子類訪問父類,可以直接通過成員屬性。那麼如果通過父類訪問子類呢 ? 通過一個巨集定義來實現這個功能。
/** * Get struct pointer from member pointer */ #define StructFrom2(memberPtr, structType) \ ((structType*) ((char*) memberPtr - offsetof(structType, memberPtr))) /** * Get struct pointer from member pointer */ #define StructFrom3(memberPtr, structType, memberName) \ ((structType*) ((char*) memberPtr - offsetof(structType, memberName)))
typedef struct Sprite Sprite; struct Sprite { Drawable drawable[1]; }; 1Sprite* sprite = StructFrom2(drawable, Sprite);
這樣,我們就可以,通過Sprite的父類Drawable屬性,來獲得子類Sprite的指標。其原理,是通過offsetof獲得成員偏移量,然後用成員地址偏移到子類地址上。有了這個機制,我們就可以實現多型,介面等抽象層。
我們可以在介面函式中,統一傳入父類物件,就可以拿到具體的子類指標,執行不同的邏輯,讓介面方法體現出多型特性。
第三, 多型
typedef struct Drawable Drawable; struct Drawable { /** Default 0.0f */ float width; float height; /** * Custom draw called by ADrawable's Draw, not use any openGL command */ void (*Draw) (Drawable* drawable); };
當,我們把一個函式指標放入,結構體物件的時候。意味著,在不同的物件裡,Draw函式可以替換為不同的實現。而不是像在ADrawable裡面的函式只有一個固定的實現。在子類繼承Drawable的時候,我們可以給Draw賦予具體的實現。而統一的呼叫Draw(Drawable* drawable)的時候,就會體現出多型特性,不同的子類有不懂的實現。
typedef struct { Drawable drawable[1]; } Hero;
typedef struct { Drawable drawable[1]; } Enemy;
Drawable drawables[] = { hero->drawable, enemy->drawable, }; for (int i = 0; i < 2; i++) { Drawable* drawable = drawables[i]; drawable->Draw(drawable); }
在Hero和Enemy的Create函式中,我們分別實現Draw(Drawable* drawable)函式。如果,我們有一個繪製佇列,裡面都是Drawable物件。傳入Hero和Enemy的Drawable成員屬性。在統一繪製呼叫中,drawable->Draw(drawable),就會分別呼叫Hero和Enemy不同的draw函式實現,體現了多型性。
第四,重寫父類方法
在繼承鏈中,我們常常需要重寫父類方法,有時候還需要呼叫父類方法。
typedef struct { Sprite sprite[1]; } SpriteBatch;
比如,SpriteBatch 繼承 Sprite, 我們需要重寫Draw方法,還需要呼叫Sprite的Draw方法。那麼我們就需要把Sprite的Draw方法公佈出來。
typedef struct { Drawable drawable[1]; } Sprite; typedef struct { void (*Draw)(Drawable* drawable); } _ASprite_; extern _ASprite_ ASprite;
這樣,每個Sprite的Draw方法可以,通過ASprite的Draw訪問。
// override static void SpriteBatchDraw(Drawable* drawable) { // call father ASprite->Draw(drawable); } spriteBatch->sprite->drawable->Draw = SpriteBatchDraw;
那麼,SpriteBatch就重寫了父類的Draw方法,也能夠呼叫父類的方法了。
第五,記憶體管理
一個malloc對應一個free,所以Create出來的物件需要自己手動free。關鍵是,在於組合的情況。就是物件內有別的物件的指標,有些是自己malloc的,有些是共用的。其實,計數器是一個簡單的方案,但我仍然覺得複雜了。在想到更好的方案之前,我傾向於更原始的手動方法,給有需要記憶體管理的物件新增Release方法。
typedef struct { Drawable* parent; Array* children; } Drawable; typedef struct { void (*Release)(Drawable* drawable); } _ADrawable_; extern _ADrawable_ ADrawable;
Drawable 含有兩個指標, 一個是parent可能別的物件也會使用,所以這個parent在Release函式中不能確定釋放。還有一個children這個陣列本身是可以釋放的,所以在Create函式裡,我們自己malloc的,都要在Release方法裡自己free。
所以,對於Create方法我們需要free + Release。對於Init 只需要呼叫Release方法就可以釋放完全了。那麼,parent這種公用的指標,就需要paren物件自己在合適的時機去釋放自己。肯定沒有計數器來的方便,但是這個足夠簡單開銷也很小。
另外本人從事線上教育多年,將自己的資料整合建了一個QQ群,對於有興趣一起交流學習c/c++的初學者可以加群:941636044,裡面有大神會給予解答,也會有許多的資源可以供大家學習分享,歡迎大家前來一起學習進步!