什麼是 object model?
**簡單的說就是用來描述 object 的結構. 所有在語法上提供了 object oriented paradigm 支援的 programming language 都會用到它 (雖然你不會自行去將它做出來), 但它確實會出現在以下場合
- new 一個 object 時要配置多少 memory? 怎麼為它進行初始的動作?
- delete 一個 object 前相關的 resource 怎麼釋放?
- 存取一個 object 時, member variable 在 object 中的什麼位置?
- Class hierarchy 怎麼表現?
- Polymorphic 如何表現?
**
C 語言哪來的 “Object Model”?
C 本身雖然沒有 OO 的語法, 但靠著本身簡潔的設計, 使它有非常大的彈性可以用在多種 programming paradigm. 做這樣的練習, 除了好玩外, 在實務上, 許多的 open source project 也大量的採用了這樣的設計手法 (雖然實現上各有不同), 是個能大大增加 OO 瞭解深度的好方法.
事先要準備什麼?
一個標準, 夠新的 C compiler. 雖然不是強制性的, 但可以優先考慮 gcc or clang.
簡單的複習一下 OO 術語

Shape Class
**程式觀念/語言的學習直接以例子學習最快. 假設今天我們想寫個繪圖程式, 第一件事會想到怎麼保存使用者的畫出的各種型狀, 例如點, 線, 圓, 三角型, 方型.... 下面是範例中用到的 class 階層關係
以 OO 表現時, 我們可以為所有的型狀宣告一個 root class 叫 Shape. 這個 Shape 是個 abstract class , 有個顏色屬性, C++ 寫成的 Shape長成這樣class Shape{private: uint32_t argb;
private: Shape() : argb(0xffffffff) {}
public: uint32_t get_argb() const { return argb; } void set_argb(uint32_t argb) { this->argb = argb; }};
要以 C 來改寫的話, 先分析一下上面這個 class 帶有些什麼資訊
- Abstract class 不能被 instantiate, 因此將 constructor 宣告為 private
- 放置資料用的 member variable (rgba)
- 初始 member variable 的地方 (constructor)
- argb 的 getter/setter. 其中 getter 是個 const function (執行後不會對 object 本身留下影響)
先處理資料的部份. 可以放在一個叫 Shape 的 struct 中typedef struct _Shape Shape;
struct _Shape{ uint32_t argb;};
接下來用提供替代 constructor 用的 functionvoid shape_init(Shape * self){ assert(NULL != self);
self->argb = 0xffffffff;}
存取 argb 用的 getter/setteruint32_t shape_get_argb(const Shape * self){ assert(NULL != self);
return self->argb;}
void shape_set_argb(Shape * self, uint32_t argb){ assert(NULL != self);
self->argb = argb;}
因為不能被 instantiate 所以就沒有提供配置及釋放的 function 了. 這樣, C 版的 Shape class 就完成了. 幾個重點
- 在 C++ 中, instance function 都會有個隱含的參數叫 this, 是個指向 object 本身的 pointer. 在我們以 C 完成的版本中, 它改名叫 self 以第一個參數傳入
- 加上的 assert() 只是個人習慣. 在 C++ 中語言本身並沒有這樣的檢查
- C++ 中的 const function, 我們可將 self 參數宣告為 const 來應對
Source code 在此
**
Point Class
**說到 OO 特徵, 第一個會想到的可能是 inheritance. 接下來加上個 Shape 的 subclass 叫 Point. Point 除了有 argb 屬性外, 還加上了 x 及 y. C++ 版如下class Point : public Shape{private: float x; float y; public: Point() : x(0.0f), y(0.0f) {} float get_x() const { return x; } void set_x(float x) { this->x = x; } float get_y() const { return y; } void set_y(float y) { this->y = y; }};
應該不難理解才是. 跟 Shape 的差異有
- 是個 concrete class, 可被 instantiate
- Superclass 為 Shape
- 多了兩個屬性 x 及 y
用 C 來表現 Point 跟 Shape 的關係要怎麼完成呢? 考量到效率 (寫 code 的效率, 執行的效率) 還有 code reuse, 我們不會希望有類似這樣的做法typedef enum _ShapeType ShapeType;
enum _ShapeType ShapeType{ SHAPE_TYPE_TOP, SHAPE_TYPE_POINT};
void shape_get_argb(void * shape, ShapeType shape_type){ switch(shape_type) { case SHAPE_TYPE_TOP: return ((Shape *) shape)->argb; case SHAPE_TYPE_POINT: return ((Point *) shape)->argb; cefault: assert(0); }}
如果仔細的思考過 inheritance 的本質, 其實不過是資料結構上的延伸, 所以這麼做應該是很自然的typedef struct _Point Point;
struct _Point{ Shape base; float x; float y;};
在 memory 中看起來像這樣
也就是說當我有個 pointer 指向 Point object 時, 我是可以安全的將它 cast 成 Shape *, 並存取到 Point 開頭 Shape 的 argb, 效果等同於 self->base.argb;
Point 初始的部份. 這裡就利用了上面所說的特性, 將 self cast 成 Shape * 後重用 shape_init(), 讓 Shape 做它自己那部份的初始void point_init(Point * self){ assert(NULL != self); shape_init((Shape *) self); self->x = 0.0f; self->y = 0.0f;}
x, y 的 getter/setterfloat point_get_x(const Point * self){ assert(NULL != self); return self->x;}
void point_set_x(Point * self, float x){ assert(NULL != self); self->x = x;}
float point_get_y(const Point * self){ assert(NULL != self); return self->y;}
void point_set_y(Point * self, float y){ assert(NULL != self); self->y = y;}
因為 Point 是 concrete class, 我們必須提供 new 及 free function, 以免 class 的 user 重覆的寫 malloc() + point_init() 這兩個步驟Point point_new(){ Point * self = malloc(sizeof(Point)); if(self) { point_init(self); } return self;}
void point_free(Point * self){ free(self);}
實測看看Point * point = point_new();printf("point->base.argb = 0x%x, shape_get_argb() = 0x%x\n", point->base.argb, shape_get_argb((Shape *) point));
執行結果point->base.argb = 0xffffffff, shape_get_argb() = 0xffffffff
看來與預期相符.
先前在教同事時, 有提過一個問題: 如果將 Point 中的 Shape 改成 pointer, 指向 new 出來的 Shape, 還能以這麼簡捷的方式做到我們要的功能嗎? 可以自行實驗看看.
Source code 在這裡
**
Circle Class
**再加個 Circle class. Circle class 是 Point 的 subclass (只為 demo 用, 並不是好設計), 也是 concrete class, 可被 instantiate, 加上了 radius 屬性. 這裡是 C++ 版本class Circle : public Point{private: float radius; public: Circle() : radius(1.0f) {} float get_radius() const { return radius; } void set_radius(float radius) { this->radius = radius; }};
C 版本. 之後 cast 的動作會做愈來愈頻繁, 為了之後方便還有 code 的簡潔, 我們可以定義幾個 cast 用的 macro#define SHAPE(o) ((Shape *) (o))#define POINT(o) ((Point *) (o))#define CIRCLE(o) ((Circle *) (o))
typedef struct _Circle Circle;
struct _Circle{ Point base; float radius;};
void circle_init(Circle * self){ assert(NULL != self); point_init(POINT(self)); self->radius = 1.0f;}
Circle * circle_new(){ Circle * self = malloc(sizeof(Circle)); if(self) { circle_init(self); } return self;}
void circle_free(Circle * self){ free(self);}
float circle_get_radius(const Circle * self){ assert(NULL != self); return self->radius;}
void circle_set_radius(Circle * self, float radius){ assert(NULL != self); self->radius = radius;}
Circle 在 memory 中的 layout現在我們的 class hierarchy 有 3 層了, cool!
Source code 在這裡
**
Polymorphic
**目前為止, 除了資料的部份經由 inheritance 延伸了, 為新的 class 加了額外的 function 之外, 似乎還沒看出什麼 OO 的好處. 試想, 如果我們有一堆的 Shape object (不管是 Point 還是 Circle) 放在 Shape * 型態的 array 中, 如果想一口氣把它們都畫出來 (draw), 怎麼做? 每一種 Shape object 的畫法應該都會不同, 你覺得這樣可行嗎?typedef _ShapeType ShapeType;enum _ShapeType{ SHAPE_TYPE_POINT, SHAPE_TYPE_CIRCLE,};
struct _Shape{ ShapeType type; uint32_t argb;};
void shape_draw(Shape * shape){ assert(NULL != shape);
switch(shape->type) { case SHAPE_TYPE_POINT: point_draw(POINT(shape)); break; case SHAPE_TYPE_CIRCLE: circle_draw(CIRCLE(shape)); break; default: assert(0); }}
能 work, 但不是很優的解決方式. 可以預見的幾個問題有
- 每個要操作 Shape 的 function 都得要有個 switch/case 在其中
- 每當新增一種 Shape, 每個 funtion 中的 swith/case 就得要跟著改
C++ 的解法是, virtual function. Class 改寫如下class Shape{ …
public: virtual void draw() = 0;};
class Point : public Shape{ ...
public: virtual void draw() { cout << "Point\n"; }};
class Circle : public Point{ …
public: virtual void draw() { cout << "Circle\n"; }}
用 C++ 要達到我們希望的功能很簡單Shape * shapes[] = { new Point(), new Circle() };for(int i = 0; i < sizeof(shapes) / sizeof(shapes[0]); i ++) { shapes[i]->draw(); delete shapes[i];}
很漂亮的完成了工作 - 沒有 if/else, 沒有 switch/case, 以下是執行結果PointCircle 用 C 要達到 virtual function 等同的效果, 得用上 function pointer. 只要改 Shape class 即可struct _Shape{ uint32_t argb; void (* draw)(Shape * self);};
再為 Shape class 加個 call 這個 virtual function 用的 functionvoid shape_draw(Shape * self){ assert(NULL != self); assert(NULL != self->draw); self->draw(self);}
改寫 point_init() 及 circle_init(), 讓 draw 指向不同的 functionstatic void point_draw(Shape * self){ printf("Point\n");}
void point_init(Point * self){ assert(NULL != self); shape_init(SHAPE(self)); SHAPE(self)->draw = point_draw; self->x = 0.0f; self->y = 0.0f;}
static void circle_draw(Shape * self){ printf("Circle\n");}
void circle_init(Circle * self){ assert(NULL != self); point_init(POINT(self)); SHAPE(self)->draw = circle_draw; self->radius = 1.0f;}
試試看能不能達到我們的需求Shape * shapes[] = { SHAPE(point_new()), SHAPE(circle_new()) };int i;for(i = 0; i < sizeof(shapes) / sizeof(shapes[0]); i ++) { shape_draw(shapes[i]); /* point_free() or circle_free()? */}
執行結果PointCircle
範例看起來跟 C++ 版相當的類似, 不錯. 但, 在過程中還是發現了一些待解問題
- Function pointer 放在 object 中, 造成 object size 變大
- Function pointer 放在 object 中, 造成 object 初始過程變複雜
- 雖然都是 Shape 的 subclass, 但 Point 及 Circle object 的 destruct 過程不一致
嗯, 雖然還有改進空間, 但應該能感受到 OO 中 polymorphic 的好處了, 愈來愈有 OO 的味道啦~
Source code 在此
**
Class Object
Review 一下目前的成果, 明顯有以下兩個問題
- 無法重用的 object instantiate 流程
- 無法重用的 object destroy 流程
對於第一點, 我們應該只要知道 object 的 size 就可以讓 malloc() 的呼叫集中在一處了. 而 object 的 construction 及 destruction 則可以增加兩個 virtual function 應對 (加上 draw() 就有 3 個 virtual functon 了). 如果這些 virtual function 都丟在 object 中的話, 在 runtime 會造成資源的浪費. 舉個例子, 如果我們有 10 個相同 class 的 object, 10 個 virtual function, 在 32bit 環境下每個 function pointer 佔 4 bytes, 因此光這些 function pointer 就花了 4 * 10 * 10 = 400 bytes. 明明這些 virtual function 都是指向同一組 function, 如果能集中共用的話, 就能大大的節省資源了.
C++ 是以 vtable 及 vptr 配合達成, 後者放在 object 中, 指向一張同 class 間共用的 vtable, 如下圖用 C 來處理以上的問題, 對目前的設計要做不少改變. 首先要做的是導入 Class object - 用來描述 class 用的 object, 裡面我們可以放入
- 指向 superclass 的 pointer: 在 construct 跟 destruct 的過程會用到
- Class name
- Instance size
- Constructor
- Class flags: 提供如這個 class 是否為 concrete class 或是 abstract class 之類的資訊
宣告如下#define OBJECT_CLASS(c) ((const ObjectClass ) (c))
typedef enum _ClassFlags ClassFlags;
typedef struct _ObjectClass ObjectClass;
typedef void ( InstanceInitFunc)(void * object);
enum _ClassFlags{ CLASS_FLAGS_NONE = 0, CLASS_FLAGS_ABSTRACT = 1 << 1,};
struct _ObjectClass{ const ObjectClass * parent; const char * name; size_t inst_size; InstanceInitFunc inst_init; ClassFlags flags;};
當 ObjectClass 被設定 CLASS_FLAGS_ABSTRACT 就代表這個 class 無法用來生成 object (uninstantiatable).
再加個 Object class 作為 root class#define OBJECT(o) ((Object ) (o))
typedef struct _Object Object;
struct _Object{ const ObjectClass * clazz;};
為 Object class 填充 Class object ;)const ObjectClass object_class = { NULL, / parent / "Object", / name / sizeof(Object), / instance size / NULL, / constructor / CLASS_FLAGS_ABSTRACT / Object is a abstract class */};
Object 的 clazz 成員指向所屬的 Class object, 這樣, 當拿到一個 object 時, 就可以用 clazz 去辨識它是什麼 class 了, 加個 function 方便存取const void * object_get_class(void * self){ assert(NULL != self); return OBJECT(self)->clazz;}
最後, 加上 function 讓我們可用特定的 Class object 產生/釋放 instancevoid * object_create_instance(const Class * clazz){ assert(NULL != clazz); assert(! (CLASS_FLAGS_ABSTRACT & clazz->flags)); assert(sizeof(Object) <= clazz->inst_size); void * self = malloc(clazz->inst_size); if(self) { OBJECT(self)->clazz = clazz; object_init(self, clazz); } return self;}
void object_free(void * self){ free(self);}
比較複雜的地方是執行 construct 動作的 object_init() 這行. Object 的初始, 必須從 root class 往下向目前的 class 執行. 而 destruct 則是由目前 class 向上一直到 root class 的方向執行. 為了簡化, 這裡用 recursive 完成 constructor 的呼叫static void object_init(void * self, const Class * clazz){ if(clazz->parent) { object_init(self, clazz->parent); } if(clazz->inst_init) { clazz->inst_init(self); }}
這樣基礎架構就完成了一部份. 但要能 new 出 object, Shape, Point 及 Circle 都得要修改. 修改的重點有
- xxx_new() 不再需要自行 malloc() 所需的 memory, 而是轉呼叫 object_create_instance()
- 不需要在 xxx_new() 中呼叫 xxx_init(), object_create_instance() 幫我們完成了
- 增加一個 ShapeClass 用以放置 virtual function draw()
- 將 Shape 的 superclass 改為 Object class
- 去掉 xxx_free(), 所有 Object class 的 subclass 都用 object_free() 做一致的 destroy 動作
先增加 ShapeClass, 將 virtual function draw() 移到這裡面#define SHAPE_CLASS(c) ((ShapeClass *) (c))
typedef struct _ShapeClass ShapeClass;
struct _ShapeClass{ Class base;
void (* draw)(Shape * self);};
將 Object 做為 Shape 的 superclass.typedef struct _Shape Shape;
struct _Shape{ Object base; uint32_t argb;};
填充 ShapeClassconst ShapeClass shape_class = { OBJECT_CLASS(& object_class), /* parent / "Shape", / name / sizeof(Shape), / instance size / (InstanceInitFunc) shape_init, / constructor / CLASS_FLAGS_ABSTRACT, / Shape is uninstantiatable / NULL / No default draw() function */};
shape_init() 因為是在 construct 時自動呼叫, 呼叫前有先進行 NULL pointer 的檢查, 因此不用在其中 assert(NULL != self) 了. 另外, 更重要的是 shape_init() 只要專注在初始自己的部份, 無論它是否被包含在 Point or Circle 之中static void shape_init(Shape * self){ self->argb = 0xffffffff;}
改寫 shape_draw() function, 經由 ShapeClass 呼叫 draw() 而非原來的 Shapevoid shape_draw(Shape * self){ assert(NULL != self); assert(NULL != SHAPE_CLASS(object_get_class(slef))->draw); SHAPE_CLASS(object_get_class(slef))->draw(self);}
這樣, Shape 的修改過一段落. 接下來是 Point class. 增加 PointClass#define POINT_CLASS(c) ((PointClass ) (c))
typedef struct _PointClass PointClass;
struct _PointClass{ ShapeClass base;};
Point class 跟 Object class 的關係經由 Shape class 完成, 所以不用修改. 接下來填充 PointClassconst PointClass point_class = { OBJECT_CLASS(& shape_class), / parent / "Point", / name / sizeof(Point), / instance size / (InstanceInitFunc) point_init, / constructor / CLASS_FLAGS_NONE, / class flags / point_draw / virtual function draw */};
因為現在有 type information 了, 可以修改 point_new() 讓它傳回 Shape * 而不是 Point * 方便使用, 並改呼叫 class_create_instance() 來產生 objectShape * point_new(){ return object_create_instance(CLASS(& point_class));}
因為許多資訊移到了 Class 中, object_create_instance() 也幫忙做了很多事, 所以 point_new() 可以變得這麼簡單. 再去掉 point_free(), Point class 工作就告一段落.
Point class 的修改過一段落. 接下是 Circle class, 改法跟 Point class 一樣, 加 CircleClass, 填 CircleClass, 改 circle_new(), 去掉 circle_free()#define CIRCLE_CLASS(c) ((CircleClass ) (c))
typedef struct _CircleClass CircleClass;
struct _CircleClass{ PointClass base;};
const CircleClass circle_class = { OBJECT_CLASS(& point_class), / parent / "Circle", / name / sizeof(Circle), / instance size / (InstanceInitFunc) circle_init, / constructor / CLASS_FLAGS_NONE, / Circle is a concrete class / circle_draw / virtual function draw */};
改 circle_new()Shape * circle_new(){ return object_create_instance(OBJECT_CLASS(& circle_class));}
去掉 circle_free() 後, 改寫先前的 demoShape * shapes[] = { point_new(), circle_new() };int i;for(i = 0; i < sizeof(shapes) / sizeof(shapes[0]); i ++) { shape_draw(shapes[i]); object_free(shapes[i]);}
執行結果PointCircle
非常的接近 C++ 版本的範例了, 是吧! 最後, 用圖示一下這些 struct 的關係
Source code 在此