天天看點

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 語言面向對象程式設計系列的基礎性介紹就告一段落,下面是前幾篇的連結,有興趣的可以回頭看看:

  • C語言面向對象程式設計(一):封裝與繼承
  • C語言面向對象程式設計(二):繼承詳解
  • C語言面向對象程式設計(三):虛函數與多态

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