2017年5月27日 星期六

Object-Oriented Programming in C (Part 1)

最近呢,希望可以用 C 來實做我的 project,遇到了不少問題。其中,像是 class 的改寫。
學習、查找資料一陣子。現在大概知道要怎麼實作了。


  1. OO 是什麼?
    我的理解是,OO 是一種抽象化的編程方式。希望可以在一個比較概念的「空中」來設計程式。這裡引用 Design Pattern 的一句話 “Program to an interface, not an implementation.” 也就是,將「實做」與「界面」分開。如此一來在代碼的重複使用、開發及維護上都會比較方便。
    定義一個簡單的界面,將會包含這個界面的資料及使用方式。實作上,就是將「資料」與「方法」包裝起來。
  2. 實例說明:
    對我來說,有個實作的程式會比較清楚。
    如果我們想要定義長方形、三角形 在 C++ 中:
    // polygon.hpp class Rect { public: // variables int width; int height; // methods int area(); }; class Trig { public: // variables int width; int height; // methods int area(); };
    // polygon.cpp #include <iostream> #include "polygon.hpp" // constructor Rect::Rect(){} // destructor Rect::~Rect(){} int Rect::area(){ return this->width * this->height; } Trig::Trig(){} Trig::~Trig(){} int Trig::area(){ return this->width * this->height / 2; } using namespace std; int main(){ Rect rect = Rect(); Trig trig = Trig(); rect.width = 4; rect.height = 5; trig.width = 4; trig.height = 5; cout << "rect area: " << rect.area() << endl; cout << "trig area: " << trig.area() << endl; return 0; }
    $ g++ polygon.c -o polygon.run
    $ ./polygon.run
    rect area: 20
    trig area: 10
    
    在 C 中,並沒有 class。但我們可以用 sturct 來實現類似的功能。由於 struct 只能包含 “value”,界面的方法可以用 function to pointer 來表達:
    // polygon.h typedef struct Rect_struct Rect; // constructor struct Rect_struct { // variables int width; int height; // methods int (*area)(Rect * self); // destructor int (*free)(Rect * self); }; typedef struct Trig_struct Trig; // constructor Trig * Trig_init(); struct Trig_struct { // variables int width; int height; // methods int (*area)(Trig * self); // destructor int (*free)(Trig * self); };
    如此,便可以實現「資料 + 方法」的包裝。
    界面定義好後,可以來處理實做的部份:
    // polygon.c #include <stdlib.h> #include "polygon.h" // Rectangle static int rect_area_impl(Rect * self){ return self->width * self->height; } static int rect_free_impl(Rect * self){ free(self); return 0; } Rect * Rect_init(){ Rect * self = NULL; self = malloc(sizeof(Rect)); self->width = 0; self->height = 0; self->area = rect_area_impl; self->free = rect_free_impl; return self; } // Triangle static int trig_area_impl(Trig * self){ return self->width * self->height / 2; } static int trig_free_impl(Trig * self){ free(self); return 0; } Trig * Trig_init(){ Trig * self = NULL; self = malloc(sizeof(Trig)); self->width = 0; self->height = 0; self->area = trig_area_impl; self->free = trig_free_impl; return self; }
    這裡用了一個技巧,使用 static 宣告函數的話,此函數只有本文件可見。如此便將實做給「隱藏」起來。
    使用上:
    // main.c #include <stdio.h> #include "polygon.h" int main(void){ Trig * trig = Trig_init(); Rect * rect = Rect_init(); trig->width = 4; trig->height = 5; rect->width = 4; rect->height = 5; printf("area of trig: %d\n",trig->area(trig)); printf("area of rect: %d\n",rect->area(rect)); trig->free(trig); rect->free(rect); }
    如此使用上可以看見與 C++ 相似,實做雖然較長,但非常清楚。
    執行:
    $ gcc -Wall -c polygon.c
    $ gcc -Wall -g polygon.o main.c -o polygon.run
    $ ./polygon.run
    area of trig: 10
    area of rect: 20
    
  3. Inherent and Polymorphism
    繼承與多形,這是物件導向作法強大的地方,那如何用 C 實做呢?
    前面可以看見,在操作上,由於定義好了界面,所以使用上非常簡單。但是在開發上,有許多代碼看起來是重複的,或者說,三角形和長方形有相同性質,他們都是多邊形。
    因此,在 C++ 中我們可以先定義一個 base 界面,或「父類別」
    // polygon.hpp // Base class class Polygon { public: int width, height; void set_values(int w, int h); int area(); }; class Rect: public Polygon{ public: int area(); }; class Trig: public Polygon{ public: int area(); };
    可以看見,Rect 和 Trig 都繼承了 Polygon 的屬性。但是,長方形和三角形的面積算法不同,同樣呼叫了 area() ,會使用不同的代碼,這便是多形(polymorphism)。
    因此在實作上,
    // polygon.cpp #include <iostream> #include "polygon.hpp" void Polygon::set_values(int width, int height){ this->width = width; this->height = height; } int Polygon::area(){ return 0; } int Rect::area(){ return this->width * this->height; } int Trig::area(){ return this->width * this->height / 2; }
    對於 area() 我們只需要重新定義他的行為就可以了。
    使用上,
    // main.cpp #include <iostream> #include "polygon.hpp" using namespace std; int main(){ Rect rect; Trig trig; rect.set_values(4,5); trig.set_values(4,5); cout << "Area of rect: " << rect.area() << endl; cout << "Area of trig: " << trig.area() << endl; return 0 }
    $ g++ -Wall -c polygon.cpp
    $ g++ -Wall -g polygon.o main.cpp -o polygon.run
    $ ./polygon.run
    Area of rect: 20
    Area of trig: 10
    
    在使用方式相同的情況下,我們不需要在重複定義,而且代碼整體也非常整潔、易維護。
    那如何用 C 實做呢?
    Follow 相同的想法,先定義 polygon
    // polygon.h // Base class: polygon typedef struct polygon polygon; // constructor polygon * polygon_init(void); struct polygon { // destructor void (*free)(polygon * self); // variables int width, height; // methods void (*set_values)(polygon * self,int w, int h); int (*area)(polygon * self); };
    如此一個通用界面便定義好了,在其他的 derived class 只要引用即可。
    // Derived class: Rectangle typedef struct Rect Rect; Rect * Rect_init(void); struct Rect{ polygon base; // derived from polygon void (*free)(Rect *); }; // Derived class: Triangle typedef struct Trig Trig; Trig * Trig_init(void); struct Trig { polygon base; // derived from polygon void (*free)(Trig*); };
    實作上,
    // polygon.c #include <stdlib.h> #include "polygon.h" static void polygon_free_impl(polygon * self){ free(self); } static void polygon_set_values_impl(polygon * self, int width, int height){ self->width = width; self->height = height; } static int polygon_area_impl(polygon * self){return 0;} polygon * polygon_init(void){ polygon * new = calloc(1,sizeof(polygon)); if(NULL == new){exit(EXIT_FAILURE);} new->width = 0; new->height = 0; new->set_values = polygon_set_values_impl; new->area = polygon_area_impl; new->free = polygon_free_impl; return new; } // Rect static void Rect_free_impl(Rect * self){ if(self) free(self); } static int Rect_area_impl(polygon * self){ return self->width * self->height; } Rect * Rect_init(void){ Rect * new = calloc(1,sizeof(Rect)); if(NULL == new){exit(EXIT_FAILURE);} polygon * base = polygon_init(); new->base = * base; new->base.area = Rect_area_impl; new->free = Rect_free_impl; return new; } // Trig static void Trig_free_impl(Trig * self){ if(self){free(self);} } static int Trig_area_impl(polygon * self){ return self->width * self->height / 2; } Trig * Trig_init(void){ Trig * new = calloc(1,sizeof(Trig)); if(NULL == new){exit(EXIT_FAILURE);} polygon * base = polygon_init(); new->base = * base; new->base.area = Trig_area_impl; new->free = Trig_free_impl; return new; }
    可以看見 C 的實做更長、更複雜,但主要是在 constructor 及 destructor 的地方,我們必須給出明確的定義。
    使用上,
    // main.c #include <stdio.h> #include "polygon.h" int main(void){ Rect * rect = Rect_init(); Trig * trig = Trig_init(); rect->base.set_values(&(rect->base),4,5); trig->base.set_values(&(trig->base),4,5); printf("Area of rect: %d\n",rect->base.area(&(rect->base))); printf("Area of trig: %d\n",trig->base.area((polygon*)trig)); rect->free(rect); return 0; }
    $ gcc -c -g -Wall polygon.c
    $ gcc -g -Wall polygon.o main.c -o polygon.run
    $ ./polygon.run
    Area of rect: 20
    Area of trig: 10
    
    基本上,使用的原則是一樣的,但是各個 input 的 type。這讓界面變得不是這麼統一而難使用。
    另外,這裡可以看到我用了兩種不一樣的方式來呼叫 base 的 method,
    rect->base.area(&(rect->base));
    trig->base.area((polygon*)trig);
    
    這是由於在一開始時我們將 base 放在 structure 的最前面,因此在記憶體中,base 的初始位置和 derived class 是相同的。藉由這個技巧,我們可以設計一個簡單的 Object 實作,讓編程及使用都更簡單。[4]

reference:

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

沒有留言:

張貼留言

歡迎發表意見