2017年5月30日 星期二

Object-Oriented Programming in C (Part 2)

HackMD - Collaborative markdown notes
上次說到,用 C 來實作繼承及多形時,界面不統一,並且由於冗長的原故,反而變得難以閱讀及維護。這樣雖然可以用 OO 的概念來開發,但是如果代碼變成如此難以使用反而是違背了初衷呢。
上次最後的例子可以發現,程式在宣告記憶體時,第一個 attribute 和 class 的位置是相同的。應用這點,我們可以大幅簡化使用方式,並且讓代碼清晰可用。

1. 定一通用的 class 界面

定義一個通用的界面,可以幫助我們大幅度減少代碼的重寫。在這邊主要是定義了 constructor 以及 destructor
// object.h #pragma once typedef struct Object Object; struct Object { int (*init)(void * self); void (*free)(void * self); }; void * Object_new(size_t size, Object Base); #define _(N) base.N #define NEW(T) Object_new(sizeof(T), T##Init) #define DEL(N) ((Object*)N)->free(N)
這裡用到一些有趣的巨集(Macro),這裡一一解釋,
#progma once
這是為了避免重複編譯,也可以用
#ifndef OBJECT_H
#define OBJECT_H
...
#endif
第二種方法在各編譯器都是通用的,適合移植。但是在編寫大型程式時可能碰上 Macro 名相同的麻煩。第一種就沒這個問題,而且現在大多編譯器也支持這種格式。
#define _(N) base.N #define NEW(T) Object_new(sizeof(T), T##Init) #define DEL(N) ((Object*)N)->free(N)
最後的 macro 主要是為了方便使用而設,待會可以見到。由 NEW 可以看見,我們之後的記憶體宣告,其實都是使用 Object_new, 但是大小由 sizeof() 決定。_(N) 就像一個新的操作符,如此我們便不需要重複的寫 base.N,也讓界面更清晰、簡單。

2. 通用界面的實作

// object.c #include <stdlib.h> #include "object.h" static void Object_free_impl(void * self){ Object * obj = self; free(obj); } static int Object_init_impl(void * self){ // do nothing return 1; } void * Object_new (size_t size, Object base){ if(!base.init) {base.init = Object_init_impl;} if(!base.free) {base.free = Object_free_impl;} Object * new_obj = calloc(1,size); * new_obj = base; if(!new_obj->init(new_obj)) { new_obj->free(new_obj); return NULL; } else{ return new_obj; } }

3. 使用方式

同樣的,我們以多邊形、長方形、三角形為例。
首先是界面,
// polygon.h #pragma once #include "object.h" // constructor Object PolygonInit; typedef struct Polygon Polygon; struct Polygon { // base class Object base; // variables int width, height; // methods void (*set)(void * self,int w, int h); int (*area)(void * self); }; Object RectInit; typedef struct Rect Rect; struct Rect{ Polygon base; }; Object TrigInit; typedef struct Trig Trig; struct Trig{ Polygon base; };
要注意的是,這裡的 constructor 是一個 Object structure,而其中初始化的定義我們可以使用 static function 來隱藏。
// polygon.c #include <stdlib.h> #include "object.h" #include "polygon.h" static void Polygon_set_impl(void * self,int w,int h){ Polygon * polygon = self; polygon->width = w; polygon->height = h; } static int Polygon_area_impl(void * self){ return 0; } static int Polygon_init(void * self){ Polygon * polygon = self; polygon->width = 0; polygon->height = 0; polygon->set = Polygon_set_impl; polygon->area = Polygon_area_impl; return 1; } Object PolygonInit = { .init = Polygon_init }; static int Rect_area_impl(void * self){ Rect * rect = self; return rect->_(width) * rect->_(height); } static int Rect_init(void * self){ Rect * rect = self; // initialize base class Polygon_init(rect); rect->_(area) = Rect_area_impl; return 1; } Object RectInit = { .init = Rect_init }; static int Trig_area_impl(void * self){ Trig * trig = self; return trig->_(width) * trig->_(height) / 2; } static int Trig_init(void * self){ Trig * trig = self; // initialize base class Polygon_init(trig); trig->_(area) = Trig_area_impl; return 1; } Object TrigInit = { .init = Trig_init };
Polygon_init() 與之前相同,我們初始化了各個變數以及方法,但是 Rect 及 Trig 只需要再次的呼叫 Polygon_init(), 並初始化自己的方法即可。
最後,
// main.c #include <stdio.h> #include "polygon.h" int main(void){ Rect * rect = NEW(Rect); Trig * trig = NEW(Trig); rect->_(set)(rect,4,5); trig->_(set)(trig,4,5); printf("Area of Rect: %d\n",rect->_(area)(rect)); printf("Area of Trig: %d\n",trig->_(area)(trig)); DEL(rect); DEL(trig); return 0; }
可以見到,使用上非常簡潔方便,而且非常清晰。如此在設計及開發都變得更容易了。
執行,
$ gcc -Wall -c object.c
$ gcc -Wall -c polygon.c
$ gcc -Wall polygon.o object.o main.c -o polygon.run
$ ./polygon.run
Area of Rect: 20
Area of Trig: 10

後記

這邊的介紹是想說明,OO 是一種編程的概念,而非一定需要什麼特定的語言才可以寫出來。但是呢,OO 的語言提供了更多的語法糖(syntax sugar),而且在許多問題都有良好的解決辦法,代碼也更加規範。但有時我們很難知道這些語法糖到底做了什麼。我還是喜歡那句話,「C 是給知道自己幹什麼的人用的語言」,用 C 實作非常的有意思,也讓我更加理解何謂 OO 。
另外這邊的實作主要是參照了[4],ex19 詳細說明了使用方式,並用這個簡單的編寫一個小遊戲,非常精彩。

reference:

  1. 我所偏爱的 C 语言面向对象编程范式
  2. C中的继承和多态
  3. 你所不知道的 C 語言:物件導向程式設計篇
  4. Learn C the hard way ex. 19 A Simple Object System

沒有留言:

張貼留言

歡迎發表意見