[C 語言] 程式設計教學:如何實作類別 (class) 和物件 (object)

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    真正的物件 (object),要有狀態 (state) 和行為 (behavior) 間的連動。狀態以資料 (data) 的形式儲存在物件的屬性 (field) 上,行為則是透過函式 (function) 來實作。和物件連動的函式,又稱為方法 (method)。

    C 語言並沒有真正的物件,只能撰寫在精神上貼近物件的函式。在本文中,我們會以平面座標中的點 (point) 為例,展示兩種物件的寫法。

    典型的寫法

    在本節中,我們展示第一種以 C 語言撰寫物件的方式,這算是主流的手法,而且實作上比較簡單。

    我們先來看外部程式如何使用 point_t 物件。假定 point_t 類別已經實作出來,在外部程式中引入 point.h 標頭檔。參考以下範例程式碼:

    #include <stdio.h>
    #include "point.h"
    
    int main(void)
    {
        point_t *a = point_new(0, 0);
        if (!a) {
            perror("Failed to allocate a\n");
            goto ERROR;
        }
    
        point_t *b = point_new(3, 4);
        if (!b) {
            perror("Failed to allocate b\n");
            goto ERROR;
        }
    
        if (!(point_distance(a, b) == 5.0)) {
            perror("Wrong distance\n");
            goto ERROR;
        }
    
        point_delete((void *) b);
        point_delete((void *) a);
    
        return 0;
    
    ERROR:
        if (b)
            point_delete((void *) b);
    
        if (a)
            point_delete((void *) a);
    
        return 1;
    }
    

    一開始,我們分別建立型別為指向 point_t 的指標的擬物件 a 和擬物件 bpoint_new() 函式在內部使用到 malloc() 函式動態配置記憶體,而 malloc() 是有可能失敗的動作,所以我們要檢查物件是否成功建立。

    接著,我們將物件 a 和物件 b 傳入 point_distance() 函式,求兩點間的距離。我們用簡單的 if 條件敘述確認兩點間的距離是正確的。

    最後,我們將物件 a 和物件 b 所占用的記憶體釋放掉。在程式要結束時回傳 0 代表程式正確地運行。

    如果程式在某段過程出錯了,我們會把程式跳到 ERROR 標籤所在的位置,走錯誤處理的流程。我們同樣會先釋放物件的記憶體,但我們不確定物件是否已建立,故使用 if 敘述檢查物件是否存在。最後,我們改回傳非零數值 1,代表程式發生錯誤。

    我們來看 point.h 標頭檔的宣告:

    #pragma once
    
    typedef struct point_t point_t;
    
    struct point_t {
        double x;
        double y;
    };
    
    point_t * point_new(double x, double y);
    void point_delete(void *self);
    double point_x(point_t *self);
    double point_y(point_t *self);
    void point_set_x(point_t *self, double x);
    void point_set_y(point_t *self, double y);
    double point_distance(point_t *a, point_t *b);
    

    一開始,我們使用 #pragma once 防止重覆引入標頭檔。雖然 #pragma once 不是標準 C 語法,很多 C 編譯器都有實作這項功能,可用來取代傳統的 #include guard。

    一開始我們利用 typedef 宣告結構體 point_t 的別名:

    typedef struct point_t point_t;
    

    由於結構體名稱和別名可以用相同的名字,建議用這種方式來宣告,看起來比較簡潔。

    接著,宣告結構體 point_t 內部的欄位:

    struct point_t {
        double x;
        double y;
    };
    

    我們按照數學上的習慣,用 xy 來命名這兩個欄位。

    接著是數個函式宣告。這些函式宣告都相當簡短,讀者可試著自己閱讀。注意我們用 point_ 來模擬命名空間,以避免函式命名衝突。

    我們來分段看 point_t 類別的實作。先看建構函式的部分:

    point_t * point_new(double x, double y)
    {
        point_t *pt = (point_t *) malloc(sizeof(point_t));
        if (!pt)
            return pt;
        
        point_set_x(pt, x);
        point_set_y(pt, y);
        
        return pt;
    }
    

    C 語言沒有真正的建構子,使用一般函式充當建構函式即可。我們常用 newcreatector 等字眼來表達該函式是建構函式,像是本範例的 point_new()

    我們在前文提過,malloc() 是有可能失敗的動作,所以要考慮失敗處理。在建構函式中,配置記憶體失敗時會回傳空指標,而配置成功時會回傳物件。外部程式可藉由判斷物件是否為空來確認物件是否成功地建立。

    我們刻意在建構函式中使用 xy 的 setter 函式來修改欄位,因為我們要確保物件的一致性。當我們更動 setter 函式的行為時,在建構函式中也可以獲得一致的行為。

    再來看解構函式的部分:

    void point_delete(void *self)
    {
        if (!self)
            return;
    
        free(self);
    }
    

    同樣地,C 語言沒有真正的解構子,使用一般函式充當解構函式即可。我們常用 deletefreedtor 等字眼來表達該函式是解構函式,像是本範例的 point_delete()

    point_t 類別的 getter 和 setter 都相當簡單:

    double point_x(point_t *self)
    {
        assert(self);
        
        return self->x;
    }
    
    double point_y(point_t *self)
    {
        assert(self);
        
        return self->y;
    }
    
    void point_set_x(point_t *self, double x)
    {
        assert(self);
        
        self->x = x;
    }
    
    void point_set_y(point_t *self, double y)
    {
        assert(self);
        
        self->y = y;
    }
    

    相信讀者可以很輕易地理解這段程式碼。

    最後來看計算距離的函式:

    double point_distance(point_t *a, point_t *b)
    {
        assert(a);
        assert(b);
        
        double dx = point_x(a) - point_x(b);
        double dy = point_y(a) - point_y(b);
        
        return sqrt(pow(dx, 2) + pow(dy, 2));
    }
    

    按照數學上的定義來計算即可,應該相當容易。

    由本節的範例可看出,C 語言的擬物件和函式之間沒有真正的連動,只是利用刻意安排,寫出具有物件感的 C 程式碼。

    替代的寫法

    在本節中,我們展示另一種實作物件的方式。這個方式稍嫌麻煩,但寫起來更有物件的精神。

    我們先看外部程式如何使用 point_t 物件。同樣地,我們假定 point_t 類別已經實作出來,引入其標頭檔 point.h 。參考範例程式如下:

    #include "point.h"
    
    int main(void)
    {
        point_class_t *cls = point_class_new();
        if (!cls) {
            perror("Failed to allocate cls\n");
            goto ERROR;
        }
        
        point_t *a = cls->new(0, 0);
        if (!a) {
            perror("Failed to allocate a\n");
            goto ERROR;
        }
        
        point_t *b = cls->new(3, 4);
        if (!b) {
            perror("Failed to allocate b\n");
            goto ERROR;
        }
        
        if (!(cls->distance(a, b) == 5.0)) {
            perror("Wrong distance\n");
            goto ERROR;
        }
    
        cls->delete((void *) b);
        cls->delete((void *) a);
        point_class_delete((void *) cls);
    
        return 0;
    
    ERROR:
        if (b)
            cls->delete((void *) b);
    
        if (a)
            cls->delete((void *) a);
    
        if (cls)
            point_class_delete((void *) cls);
        
        return 1;
    }
    

    一開始,我們不急著建立 point_t 物件,而是額外建立 point_class_t 物件,該物件用來代表 point_t 的類別。由於建立 cls 物件的建構函式內部有用到 malloc(),需檢查該物件是否成功地建立。

    接著,我們用建好的 cls 物件為類別,建立 point_t 物件 a 和物件 b。同樣地,需分別檢查兩物件是否成功地建立。

    我們將物件 a 和物件 b 傳入 cls 的方法 distance() 以求得兩點間的距離。這裡用簡單的 if 敘述檢查兩點間的距離是否有錯。

    最後,逐一將物件所占的記憶體釋放掉。在程式結束前回傳 0 代表整個程式正確地運行。

    如果程式在運行中發生問題,會跳到 ERROR 標籤所在的位置,走錯誤處理的流程。在這裡,確認物件存在後,同樣會逐一釋放物件的記憶體。但在最後回傳非零值 1,表示程式運行中發生錯誤。

    我們接著來看 point.h 的宣告:

    #pragma once
    
    typedef struct point_class_t point_class_t;
    typedef struct point_t point_t;
    
    struct point_class_t {
        point_t * (*new)(double x, double y);
        void (*delete)(void *self);
        double (*x)(point_t *self);
        double (*y)(point_t *self);
        void (*set_x)(point_t *self, double x);
        void (*set_y)(point_t *self, double y);
        double (*distance)(point_t *a, point_t *b);
    };
    
    point_class_t * point_class_new();
    void point_class_delete(void *cls);
    
    struct point_t {
        double x;
        double y;
    };
    
    point_t * _point_new(double x, double y);
    void _point_delete(void *self);
    double _point_x(point_t *self);
    double _point_y(point_t *self);
    void _point_set_x(point_t *self, double x);
    void _point_set_y(point_t *self, double y);
    double _point_distance(point_t *a, point_t *b);
    

    在標頭檔中,我們宣告兩個結構體,分別是代表類別的 point_class_t 和代表物件的 point_t

    point_class_t 類別中,我們利用函式指標宣告數個該類別的方法 (method)。

    但在 point_t 中,我們仍然要宣告相對應的函式,point_class_t 物件才能指向各個方法的實作。

    我們來看 point_class_t 類別的實作:

    point_class_t * point_class_new()
    {
        point_class_t *cls = (point_class_t *) malloc(sizeof(point_class_t));
        if (!cls)
            return cls;
        
        cls->new = _point_new;
        cls->delete = _point_delete;
        cls->x = _point_x;
        cls->y = _point_y;
        cls->set_x = _point_set_x;
        cls->set_y = _point_set_y;
        cls->distance = _point_distance;
        
        return cls;
    }
    

    在這個建構函式中,cls 本身是物件,但在外部程式中當成類別來使用。

    由這段程式碼可看出,cls 物件本身不負責實作,其方法會另外指向各個實作函式。

    雖然 cls 物件在外部程式中當成類別來用,cls 物件同樣需要自己的解構函式:

    void point_class_delete(void *cls)
    {
        if (!cls)
            return;
        
        free(cls);
    }
    

    至於各個實作函式的細節和前例相同,故不重覆展示。

    結語

    在本文中,我們展示兩種撰寫類別和物件的方法。由於物件導向程式不是標準 C 的一部分,兩種寫法都可行。讀者可以參考本文所列的方法,自己實作 C 語言的物件。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk
    標籤: C 語言, CLASS, OBJECT, OOP