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

PUBLISHED ON SEP 24, 2018 — PROGRAMMING
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    C 語言沒有內建的物件導向 (object-oriented) 語法,但我們仍然可以用 C 語言寫出有物件導向思維的語法 (可見這裡)。早期就有一本經典線上教材 Object-Oriented Programming with ANSI C 整本都在講用 C 寫物件導向程式的方式 (出處),有需要的讀者可自行前往拜讀。本系列文章以簡短的文字說明搭配短例,讓讀者很快學會使用 C 寫物件導向程式的一些手法。

    註:本系列文章所用的手法和該教材不同,讀者可自行比較。

    由於 C 語言沒有內建的物件導向語法,在各種 C 語言教材中 (包括本文) 實作物件導向程式的手法都是利用 C 語言的特性去模擬出來的;這些手法沒有一定的準則 (gold standard),也不會放到 C 標準中。最重要的不是原封不動地照抄這些手法,而是理解為什麼要用這些手法,達成了什麼效果,從中慢慢建立自己慣用的方式。

    物件 (object) 是帶有狀態 (state) 和行為 (behavior) 的抽象實體,而類別 (class) 則是建立物件的藍圖;對於類別和物件,可以想像餅乾模子和餅乾的關係。狀態透過存取屬性 (fields) 來儲存;行為透過函式 (function) 或副常式 (subroutine) 來實作。至於封裝 (encapsulation)、繼承 (inheritance)、多型 (polymorphism) 等特性,則是透過不同面向來加強物件,不是必備條件。

    在本文中,我們使用二維空間的點 (point) 來展示如何製作類別和物件,在這裡我們沒有用到進階的物件導向特性,僅是一個帶有狀態和行為的簡單物件。

    一般的寫法

    我們先看 Point 物件的使用方式:

    #include <assert.h>
    #include <stdbool.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include "point.h"
    
    int main(void)
    {
        bool failed = false;
        
        // Create a Point object.
        Point* pt = point_new(0, 0);
        if (!pt) {
            perror("Failed to allocate Point pt");
            failed = true;
            goto POINT_FREE;
        }
    
        // Check x and y.
        if (!(point_x(pt) == 0)) {
            failed = true;
            goto POINT_FREE;
        }
        
        if (!(point_y(pt) == 0)) {
            failed = true;
            goto POINT_FREE;
        }
        
        // Mutate x and y.
        point_set_x(pt, 3);
        point_set_y(pt, 4);
        
        // Check x and y again.
        if (!(point_x(pt) == 3)) {
            failed = true;
            goto POINT_FREE;
        }
        
        if (!(point_y(pt) == 4)) {
            failed = true;
            goto POINT_FREE;
        }
        
    POINT_FREE:
        // Free the object.
        point_free(pt);
        
        if (failed) {
            exit(EXIT_FAILURE);
        }
    
        return 0;
    }

    這個例子相當簡單,就是透過 Point 物件 pt 存取座標點 xy,可以看得出來物件和函式有基本的連動。

    接著,我們來看 Point 類別的公開方法,在 C 語言中透過標頭檔 (header) 來宣告某個類別的公開方法:

    #ifndef POINT_H
    #define POINT_H
    
    // Declare Point class.
    typedef struct point {
        double x;
        double y;
    } Point;
    
    // The constructor of Point.
    Point* point_new(double x, double y);
    
    // The getters of Point.
    double point_x(Point *self);
    double point_y(Point *self);
    
    // The setters of Point.
    void point_set_x(Point *self, double x);
    void point_set_y(Point *self, double y);
    
    // The destructor of Point.
    void point_free(void *self);
    
    #endif // POINT_H

    最後來看 Point 類別內部的實作:

    #include <assert.h>
    #include <stdlib.h>
    #include "point.h"
    
    // The constructor of Point.
    Point* point_new(double x, double y)
    {
        Point* pt = (Point *) malloc(sizeof(Point));
        if (!pt) {
            return pt;
        }
        
        pt->x = x;
        pt->y = y;
        
        return pt;
    }
    
    // The getter of x.
    double point_x(Point *self)
    {
        assert(self);
    
        return self->x;
    }
    
    // The setter of x.
    void point_set_x(Point *self, double x)
    {
        assert(self);
    
        self->x = x;
    }
    
    // The getter of y.
    double point_y(Point *self)
    {
        assert(self);
    
        return self->y;
    }
    
    // The setter of y.
    void point_set_y(Point *self, double y)
    {
        assert(self);
    
        self->y = y;
    }
    
    // The destructor of Point.
    void point_free(void *self)
    {
        if (!self) {
            return;
        }
            
        free(self);
    }

    本例的 Point 物件相當簡單,但精神上已經是一個物件了。即使我們完全不使用進階的物件導向特性,也可以撰寫以物件為基礎的 (object-based) 程式,利用物件來組織程式碼。

    註:一般的 object-based programming 是指有封裝但沒有繼承和多型的物件,我們採更寬鬆的定義,只要有狀態和方法連動的物件即可。

    替代的寫法

    在大部分的 C 語言教材中,皆採用上一節的寫法,因為這種寫法簡單易懂,除了用結構體模擬 this 指針外,使用一般函式的寫法即可。在本節中,我們則提供另一個替代性的寫法,雖然這個寫法比較少見,但程式碼也相當簡潔,具有一定的參考性。

    這種寫法的關鍵在於將類別 (class) 和物件 (object) 分成兩個結構體,本節同樣以二元座標系的點為例:

    #pragma once
    
    typedef struct point point_t;
    
    typedef struct point_class {
        point_t * (*new)(float, float);
        float (*x)(point_t *self);
        void (*set_x)(point_t *self, float value);
        float (*y)(point_t *self);
        void (*set_y)(point_t *self, float value);
        float (*distance)(point_t *p, point_t *q);
        void (*delete)(void *pt);
    } point_class_t;
    
    point_class_t * point_class_new(void);

    由這個宣告可以觀察到,除了原本儲存資料的 point_t 結構外,我們額外宣告一個表示類別的 point_class_t 結構。這裡使用到函式指標的手法。

    先看外部程式如何使用此點 (point) 類別和物件:

    #include <assert.h>
    #include <math.h>
    #include <stdlib.h>
    #include "point.h"
    
    int main(void)
    {
        point_class_t *klass = point_class_new();
        if (!klass)
            return 1;
    
        point_t *a = klass->new(0.0, 0.0);
        point_t *b = klass->new(1.0, 2.0);
    
        assert(klass->x(b) == 1.0);
        assert(klass->y(b) == 2.0);
    
        klass->set_x(b, 3.0);
        klass->set_y(b, 4.0);
    
        assert(fabs(klass->distance(a, b) - 5.0) < 0.00001);
    
        klass->delete(a);
        klass->delete(b);
    
        free(klass);
    
        return 0;
    }

    由於 C 語言本身的限制,我們無法將物件 ab 直接和類別結合在一起。所以用來模擬 this 指針的 ab 仍然會和函式分開。

    這個程式的好處是減少命名空間的汙染,因為所以的函式都共用 klass 這個結構,klass 在無意間同時擔任類別 (class) 和命名空間 (namespace) 的雙重角色。

    最後來看類別實作的部分:

    #include <assert.h>
    #include <math.h>
    #include <stdlib.h>
    #include "point.h"
    
    struct point {
        float x;
        float y;
    };
    
    static point_t * _point_new(float, float);
    static float _point_x(point_t *);
    static void _point_set_x(point_t *, float);
    static float _point_y(point_t *);
    static void _point_set_y(point_t *, float);
    static float _point_distance(point_t *, point_t *);
    static void _point_free(void *);
    
    point_class_t * point_class_new(void)
    {
        point_class_t *klass = (point_class_t *) malloc(sizeof(point_class_t));
        if (!klass)
            return klass;
        
        klass->new = _point_new;
        klass->x = _point_x;
        klass->set_x = _point_set_x;
        klass->y = _point_y;
        klass->set_y = _point_set_y;
        klass->distance = _point_distance;
        klass->delete = _point_free;
    
        return klass;
    }
    
    static point_t * _point_new(float x, float y)
    {
        point_t *p = (point_t *) malloc(sizeof(point_t));
        if (!p)
            return p;
        
        p->x = x;
        p->y = y;
    
        return p;
    }
    
    static float _point_x(point_t *pt)
    {
        assert(pt);
    
        return pt->x;
    }
    
    static void _point_set_x(point_t *pt, float value)
    {
        assert(pt);
    
        pt->x = value;
    }
    
    static float _point_y(point_t *pt)
    {
        assert(pt);
    
        return pt->y;
    }
    
    static void _point_set_y(point_t *pt, float value)
    {
        assert(pt);
    
        pt->y = value;
    }
    
    static float _point_distance(point_t *a, point_t *b)
    {
        assert(a && b);
    
        float x = a->x - b->x;
        float y = a->y - b->y;
    
        return sqrt(x * x + y * y);
    }
    
    static void _point_free(void *pt)
    {
        if (!pt)
            return;
        
        free(pt);
    }

    其實實作點 (point) 的部分平淡無奇,重點在於用來模擬類別的 klass 物件的産生方法。我們可以發現這個方法比傳統的手法要費更多的工,寫更多的樣板程式碼。

    你或許對以下產品有興趣
    © 2014-2019. Michael Chen
    All code in the website is licensed under Apache 2.0 unless otherwise mentioned.