以 C 語言打造 Object Model - 用不一樣的方式學 OOP - 1

什麼是 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 在此