物件導向是一種程式設計思想,雖然C並沒有提供物件導向的語法糖,但仍然可以用物件導向的思維來抽象和使用。這裡分享一套C物件導向的寫法,可以完成物件導向程式設計並進行流暢的抽象。這套寫法是在實踐中不斷調整的結果,目前已經比較穩定,進行了大量的功能編寫。
這套OOC有以下特性:
- 沒有強行去模仿c++的語法設定,而是使用C的特性去物件導向設計
- 實現繼承,組合,封裝,多型的特性
- 一套命名規範去增加程式碼的可讀性
第一,封裝
在C中可以用struct來封裝資料,如果是方法,我們就需要用函式指標存放到struct裡面來模擬。
1 2 3 4 5 6 |
typedef struct Drawable Drawable; struct Drawable { float positionX; float positionY; }; |
1 2 3 4 5 6 7 |
typedef struct { Drawable* (*Create) (); void (*Init) (Drawable* outDrawable); } _ADrawable_; extern _ADrawable_ ADrawable[1]; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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方法
第二,繼承和組合
1 2 3 4 5 6 |
typedef struct Drawable Drawable; struct Drawable { Drawable* parent; Color color[1]; }; |
- 繼承,就是在結構體裡,嵌入另一個結構體。這樣malloc一次就得到全部的記憶體空間,釋放也就一次。嵌入的結構體就是父類,子類擁有父類全部的資料空間內容。
- 組合,就是在結構體,存放另一個結構體的指標。這樣建立結構體時候,要需要呼叫父類的Create方法去生成父類空間,釋放的時候也需要額外釋放父類空間。
- 這裡parent就是組合,color就是繼承。
- 繼承是一種強耦合,無論如何子類擁有父類全部的資訊。
- 組合是一種低耦合,如果不初始化,子類只是存放了一個空指標來佔位關聯。
- 可以看到,C裡面一個結構體可以,繼承任意多個父類,也可以組合任意多個父類。
- color[1] 使用陣列形式,可以直接把color當做指標使用
子類訪問父類,可以直接通過成員屬性。那麼如果通過父類訪問子類呢 ? 通過一個巨集定義來實現這個功能。
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * 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))) |
1 2 3 4 5 |
typedef struct Sprite Sprite; struct Sprite { Drawable drawable[1]; }; |
1 |
Sprite* sprite = StructFrom2(drawable, Sprite); |
這樣,我們就可以,通過Sprite的父類Drawable屬性,來獲得子類Sprite的指標。其原理,是通過offsetof獲得成員偏移量,然後用成員地址偏移到子類地址上。有了這個機制,我們就可以實現多型,介面等抽象層。
我們可以在介面函式中,統一傳入父類物件,就可以拿到具體的子類指標,執行不同的邏輯,讓介面方法體現出多型特性。
第三, 多型
1 2 3 4 5 6 7 8 9 10 11 12 |
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)的時候,就會體現出多型特性,不同的子類有不懂的實現。
1 2 3 4 5 |
typedef struct { Drawable drawable[1]; } Hero; |
1 2 3 4 5 |
typedef struct { Drawable drawable[1]; } Enemy; |
1 2 3 4 5 6 7 8 9 10 11 |
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函式實現,體現了多型性。
第四,重寫父類方法
在繼承鏈中,我們常常需要重寫父類方法,有時候還需要呼叫父類方法。
1 2 3 4 5 |
typedef struct { Sprite sprite[1]; } SpriteBatch; |
比如,SpriteBatch 繼承 Sprite, 我們需要重寫Draw方法,還需要呼叫Sprite的Draw方法。那麼我們就需要把Sprite的Draw方法公佈出來。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
typedef struct { Drawable drawable[1]; } Sprite; typedef struct { void (*Draw)(Drawable* drawable); } _ASprite_; extern _ASprite_ ASprite; |
這樣,每個Sprite的Draw方法可以,通過ASprite的Draw訪問。
1 2 3 4 5 6 7 8 |
// override static void SpriteBatchDraw(Drawable* drawable) { // call father ASprite->Draw(drawable); } spriteBatch->sprite->drawable->Draw = SpriteBatchDraw; |
那麼,SpriteBatch就重寫了父類的Draw方法,也能夠呼叫父類的方法了。
第五,記憶體管理
一個malloc對應一個free,所以Create出來的物件需要自己手動free。關鍵是,在於組合的情況。就是物件內有別的物件的指標,有些是自己malloc的,有些是共用的。其實,計數器是一個簡單的方案,但我仍然覺得複雜了。在想到更好的方案之前,我傾向於更原始的手動方法,給有需要記憶體管理的物件新增Release方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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物件自己在合適的時機去釋放自己。肯定沒有計數器來的方便,但是這個足夠簡單開銷也很小。