位元詩人 在 C 語言模擬物件

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

C 語言沒有物件的概念,但可以用結構及函式模擬。本文透過實例展示如何模擬物件。

C# 的例子

在寫 C 程式碼前,先用真正有類別及物件的語言,觀看一下如何操作物件。這裡以 C# 為例。

使用類別建立物件

建立物件後,就可以呼叫物件的欄位或方法:

Point p = new Point(3.0, 4.0);

Assert(3.0 == p.X);
Assert(4.0 == p.Y);
Assert(5.0 == p.Magnitude);

藉由 . 運算子,物件 p 可以和其欄位或方法連動。

也可以把物件當成資料傳遞:

Point a = new Point(1.0, 2.0);
Point b = new Point(4.0, 6.0);

double dist = Point.Distance(a, b);
Assert(5.0 == dist);

透過這兩個例子,可以看出物件如何操作。

Point 的參考實作

以下是 Point 的其中一種實作方式,給讀者參考:

using static System.Math;

public class Point
{
    private readonly double _x;
    private readonly double _y;

    public Point(double x, double y)
    {
        _x = x;
        _y = y;
    }

    public double X
    {
        get
        {
            return _x;
        }
    }

    public double Y
    {
        get
        {
            return _y;
        }
    }

    public double Magnitude
    {
        get
        {
            double dx = _x - 0.0;
            double dy = _y - 0.0;

            return Sqrt(dx * dx + dy * dy);
        }
    }

    public static double Distance(Point a, Point b)
    {
        double dx = a.X - b.X;
        double dy = a.Y - b.Y;

        return Sqrt(dx * dx + dy * dy);
    }
}

這裡把 Point 視為數學上的點,建立物件後就不能改變其位置。如果把 Point 視為資料,可以改成位置可變。

在外部程式使用物件

接下來,我們在 C 操作等效物件:

point_t p;
point_new(&p, 3.0, 4.0);

assert(3.0 == point_x(&p));
assert(4.0 == point_y(&p));
assert(5.0 == point_magnitude(&p));

建構函式跟 C# 不太一樣。這牽涉到記憶體細節。詳見下文。

同樣可以將物件當成資料傳遞:

point_t a;
point_new(&a, 1.0, 2.0);
point_t b;
point_new(&b, 4.0, 6.0);

assert(5.0 == point_distance(&a, &b));

宣告 point_t 的界面

C 語言的 header 視為界面,需要額外寫出來:

#ifndef POINT_H
#define POINT_H

#ifdef __cplusplus
extern "C" {
#endif

typedef struct point_t point_t;

struct point_t {
    double _x;
    double _y;
};

void point_new(point_t *self, double x, double y);
double point_magnitude(point_t *self);
double point_distance(point_t *a, point_t *b);

#define point_x(self) ((self)->_x)
#define point_y(self) ((self)->_y)

#ifdef __cplusplus
}  /* extern "C" */
#endif

#endif  /* POINT_H */

配置記憶體

這份 header 透露了一些記憶體細節。實作 C 物件時,可以選擇直接宣告結構或用 opaque pointer 封裝。

直接宣告結構時,可以在系統堆疊上建立結構:

point_t p;
point_new(&p, 3.0, 4.0);

用 opaque pointer 時,勢必要在系統堆積配置記憶體:

point_t *p = point_new(3.0, 4.0);

一般來說,只有公開 API 需要用 opaque pointer 封裝,內部物件則不必要。

使用巨集模擬函式

這裡是巨集,而不是函式:

#define point_x(self) ((self)->_x)

使用者在編譯程式碼時,巨集會自動展開,省下函式呼叫的開銷。

point_t 的參考實作

這裡是 point_t 的其中一種實作方式:

#include <math.h>
#include "point.h"

void point_new(point_t *self, double x, double y)
{
    self->_x = x;
    self->_y = y;
}

double point_magnitude(point_t *self)
{
    double dx = point_x(self) - 0.0;
    double dy = point_y(self) - 0.0;

    return sqrt(dx * dx + dy * dy);
}

double point_distance(point_t *a, point_t *b)
{
    double dx = point_x(a) - point_x(b);
    double dy = point_y(a) - point_y(b);

    return sqrt(dx * dx + dy * dy);
}

結語

將結構視為物件導向程式的 this 指標,傳遞給函式,就可以模擬物件。這種撰碼模式在 C 語言常用,可用來組織程式碼。

關於作者

位元詩人 (ByteBard) 是資訊領域碩士,專注於從探索到產品的開發過程,並以工具驅動的方式改善專案。喜歡以開源專案作為成果,回饋社群。

主要方向包括:自用工具的打磨 (dogfooding)、編譯器技術在工具開發中的應用,以及將研究轉化為可維護的開源成果。

除了技術之外,也喜歡日本料理和黑咖啡,偶爾自助旅行,將生活中的靈感融入技術隨筆。