1. 程式人生 > >C語言面向物件程式設計(四):面向介面程式設計

C語言面向物件程式設計(四):面向介面程式設計

    Java 中有 interface 關鍵字,C++ 中有抽象類或純虛類可以與 interface 比擬,C 語言中也可以實現類似的特性。

    在面試 Java 程式設計師時我經常問的一個問題是:介面和抽象類有什麼區別。

    很多程式設計書籍也經常說要面向介面程式設計,我的理解是,介面強制派生類必須實現基類(介面)定義的契約,而抽象類則允許實現繼承從而導致派生類可以不實現(重寫)基類(介面)定義的契約。通常這不是問題,但在有一些特定的情況,看起來不那麼合適。

    比如定義一個 Shape 基類,其中定義一個 draw() 方法,給一個什麼都不做的預設實現(通常是空函式體),這實際沒有任何意義。

    再比如基類改變某個方法的實現,而派生類採用實現繼承並沒有重寫這個方法,此時可能會導致一些奇怪的問題。以鳥為例,基類為 Bird ,我們可能會定義一個 fly() 方法,一個 walk() 方法,因為有的人認為鳥既可以走又可以飛。開始時我們在 walk() 的實現裡作了假定,認為雙腳交叉前進才是 walk ,可是後來發現有些鳥是雙腳一齊蹦的,不會交叉前進。這個時候怎麼辦?基類 Bird 的 walk() 方法是否要修改、如何修改?

    在 C++ 中,沒有介面關鍵字 interface ,同時為了程式碼複用,經常採用實現繼承。在 C 語言中,我們前面幾篇文章討論了封裝、隱藏、繼承、虛擬函式、多型等概念,雖然都可以實現,但使用起來總不如自帶這些特性的語言(如 C++ 、Java )等得心應手。一旦你採用我們前面描述的方法來進行面向物件程式設計,就會發現,在 C 語言中正確的維護類層次是一件非常繁瑣、容易出錯的事情,而且要比面向物件的語言多寫很多程式碼(這很容易理解,面嚮物件語言自帶輪子,而 C 要自己造輪子,每實現一個類都要造一遍)。但有一點,當我們使用 C 語言作面向物件程式設計時,比 C++ 有明顯的優勢,那就是介面。

    介面強制派生類實現,這點在 C 中很容易做到。而且我們在程式設計中,實際上多數時候也不需要那麼多的繼承層次,一個介面類作為基類,一個實現類繼承介面類,這基本就夠了。在 C 語言中採用這種方式,可以不考慮解構函式、超過 3 層繼承的上下型別轉換、虛擬函式呼叫回溯、虛擬函式表裝配等等問題,我們所要做的,就是實現基類介面,通過基類指標,就只能操作繼承層次中最底層的那個類的物件;而基類介面,天生就是不能例項化的(其實是例項化了沒辦法使用,因為結構體的函式指標沒人給它賦值)。

    一個示例如下:

struct base_interface {
    void (*func1)(struct base_interface* b);
    void (*func2)(struct base_interface* b);
    int (*func_3)(struct base_interface* b, char * arg);
};

struct derived {
    struct base_interface bi;
    int x;
    char ch;
    char *name;
};

    上面是標頭檔案,derived 結構體通過包含 base_interface 型別的成員 bi 來達到繼承的效果;而 base_interface 無法例項化,我們沒有提供相應的建構函式,也沒有提供與 func_1 , func_2 等函式指標對應的實現,即便有人 malloc 了一個 base_interface ,也無法使用。

    derived 類可以提供一個建構函式 new_derived ,同時在實現檔案中提供 func_1 , func_2 ,func_3 的實現並將函式地址賦值給 bi 的成員,從而完成 derived 類的裝配,實現 base_interface 定義的契約。

    示例實現如下:

static void _derived_func_1(struct base_interface *bi)
{
    struct derived * d = (struct derived*)bi;
    d->x *= 2;
    printf("d->name = %s\n", d->name);
}

/* _derived_func_2 impl */
/* _derived_func_3 impl */

struct derived *new_derived()
{
    struct derived *d = malloc(sizeof(struct derived));
    d->bi.func_1 = _derived_func_1;
    d->bi.func_2 = _derived_func_2;
    d->bi.func_3 = _derived_func_3;
    d->x = 0;
    d->ch = 'a';
    d->name = NULL;

    return d;
}

    我們可以這麼使用 base_interface 介面:
void do_something(struct base_interface *bi)
{
    bi->func_1(bi);
}

int main(int argc, char **argv)
{
    struct derived * d = new_derived();
    do_something((struct base_interface*)d);

    return 0;
}

    上面的程式碼中 do_something 函式完全按照介面程式設計,而 bi 可以實際指向任意一個實現了 base_interface 介面的類的例項,在一定程式上達到多型的效果,花費的代價相當小,卻可以讓我們的程式提高可擴充套件性,降低耦合。

    這種簡單的方法也是我在自己的專案中使用的方法,效果不錯。

    好啦,C 語言面向物件程式設計系列的基礎性介紹就告一段落,下面是前幾篇的連結,有興趣的可以回頭看看:

    接下來我會提供幾個實作的例子,包括基本的資料結構,如單鏈表、樹,還有一個 http server 的例子。