前往
大廳
主題

在 C 語言裡尋找物件導向

電晶晶體 | 2023-12-10 12:39:02 | 巴幣 0 | 人氣 130

前言


有些人會把語言分門別類,物件導向的歸一邊,而非物件導向的歸一邊,像 java 是基於 class base,而 python, javascript 是基於 prototype,C++ 則是 Meta programming(因為 Template)。 並非沒有了語法支援便無法體現物件導向的精神,萬語皆有 OOP 。

實作


在 C 裡實作物件導向的語法,讓 C 看起來具備物件導向的繼承功能(實際上就是 struct 裡塞 struct)

header file : psdooc.h


#define noargs void *_noargs
#define nulargs 0

typedef struct __NullClass {} NullClass;

#define class(class_name, cls_inherit,...) \
    typedef struct __##class_name { \
        cls_inherit super; \
        __VA_ARGS__ \
    } class_name;


#define super(self) (self->super)

// class method template for function declare and define
#define method_tmpl(class_name, ret_type, method_name, ...) \
    ret_type class_name##_##method_name(class_name* self, __VA_ARGS__)


// class method call
#define method_call(obj, class_name, method_name, ...) \
    class_name##_##method_name(obj, __VA_ARGS__)

分析一下 header file。

NullClass : 空的 struct,因為我寫的巨集 class 必須要有繼承的類別(我就菜)。
巨集 noargs 跟 nulargs 跟上述原因差不多,noargs 用於 method_tmpl,而 nulargs 用於 method_call

class : 類別
  • class_name : 類別名稱
  • cls_inherit : 要繼承的類別名稱
  • ... : 變數成員(使用不定數量參數寫法)
super : 用於指向父類別,也可以直接 self->super ,看個人喜好。

method_tmpl : 聲明或定義類別方法
  • class_name : 類別名稱
  • ret_type : 回傳型態
  • method_name : 方法名稱
實際上,巨集 method_tmpl 展開後就是一個 function

method_call : 呼叫由 method_tmpl 定義的類別方法
  • obj : 要傳入的物件
  • class_name : 要呼叫方法的目標類別名稱
  • method_name : 方法名稱
  • ... : 參數(使用不定數量參數寫法)。
老實說,用 method_tmpl 跟 method_call 去重新包裝 function 讓他看起來像 method 有點多此一舉,如過要看起來更像物件的話可以在 struct 裡面塞 function pointer,如下:

typedef struct Object Object;

typedef int (*func_t)(Object *);

struct Object {
    int member_a;
    int member_b;
    func_t add, sub;// function pointer
}

int add_impl(Object *self)
{
    return self->member_a + self->member_b;
}

int sub_impl(Object *self)
{
    return self->member_a - self->member_b;
}

void func_binding(Object *obj)
{
    obj->add = add_impl;
    obj->sub = sun_impl;
}

Object *obj ObjectInit()
{
    Object *obj = (Object*) malloc(sizeof(Object));
    obj->member_a = 1;
    obj->member_b = 10;
    func_binding(obj);
    
    return obj;
}

// entry point
int main()
{
    Object *obj = ObjectInit();
    int res = obj.add(obj);
    
    free(obj);
}

這樣的寫法必須得在程式的執行階段動態將 Object 物件內的 function pointer 指向對應參數的 function,由於 C 沒有支援物件導向的語法,因此沒有像 java 有 this 這麼方便,如果物件方法要使用物件自己本身,還是得先將自己當成參數傳入。

如果不想每次都要傳入物件本身的話,可以使用巨集包裝起來,如下
#define callm(obj, func_name, ...) \
    obj->func_name(obj, __VA_ARGS__)

    
callm(car, gassing, 100);
// calling method gassing of car object

使用 function pointer 的好處是可以指向同類型,不同實做的 function,大致程度上可以達成 function overload(c++ 也是用類似的手法達成)

> 有高手實做了 C 的物件導向,想當然是一大堆看不懂的巨集 : monkc (monkey C)

example : ooc.c


使用 psdooc.h 模仿物件導向

#include "psdooc.h"
#include <stdio.h>

// class Object
class(Object, NullClass,
    int ref;

)

method_tmpl(Object, // method of class Object
            Object *, ref, noargs)
{
    return NULL;
}

method_tmpl(Object, // method of class Object
            Object *, unref, noargs)
{
    return NULL;
}

// class Vehicle
class(Vehicle, Object,
)

method_tmpl(Vehicle,
// method of class Vehicle
            void, start, noargs)
{

    if (self) printf("%x derived from %x\n", self, super(self));
}

// class Car
class(Car, Vehicle,
    int useless_a;

)

// class Truck
class(Truck, Vehicle,
    int useless_a;

    int useless_b;
)

// main
int main()
{
    Car car;

    Truck truck;

    method_call((Vehicle *) &car, Vehicle, start, nulargs);

    method_call((Vehicle *) &truck, Vehicle, start, nulargs);

    return 0;
}

結語


> 物件導向是設計上一種演化的方向,有心或沒有心,指的是開發者有沒有持續地檢討設計,以及當時的需求下是否適合朝該方向演化,而不是一味地套用封裝、繼承的語法或術語。有心的話,就算不想著物件導向,演化的方向後來自然地朝向物件導向,也只是剛好而已!

經典的 GObject 即是這種演化方向下的產物。在程式語言的演化中,不管是用哪些程式語言進行開發,不妨可以想想,究竟被限制的是語法還是自己的思考邏輯。


參考

創作回應

更多創作