程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> 純C語言實現簡單封裝繼承機制

純C語言實現簡單封裝繼承機制

編輯:關於C

0 繼承是OO設計的基礎

繼承是OO設計中的基本部分,也是實現多態的基礎,C++,C#,Objective-C,Java,PHP,JavaScript等為OO而設計的語言,其語言本身對實現繼承提供了直接支持。而遵循C/Unix設計哲學的語言,從不限定編程風格,而且提供了實現OO的基本支持。下面我們就來看看如何用C語言實現繼承。

1 內存布局層面上繼承的含義

如今幾乎所有程序員都知道繼承的抽象含義,對於被用爛了的貓狗繼承動物的例子也耳熟能詳。在此,我們拋開抽象世界,深入到繼承的具體實現上。當然不同的語言對繼承的實現機制並不完全相同,但是了解其中一種典型的實現細節對於理解繼承是非常有好處的。這裡我們以C++為例進行說明。

class B
{
    int x;
    int y;
    int z;
};
class C : B
{
    float f;
    char s[10];
};

上述代碼表示子類C繼承了父類B,下面是類C的一個實例(對象)的內存布局。
這裡寫圖片描述

C對象有兩部分組成,紅色區域是繼承自B的部分,藍色區域是自身特有的。這樣一來,紅色部分完全可以當成是一個B類對象。

2 利用結構體實現繼承的兩種方法

2.1 父類對象作為子類的成員

理解了繼承的內存布局原理之後,用C來實現繼承就非常容易了。最容易想到的方法如下:

struct B
{
    int x;
    int y;
    int z;
};

struct C
{
    struct B objB;
    float f;
    char s[10];
};

上述代碼通過在C中包含一個B類型的成員來實現繼承,此方法非常直接,但使用起來有一些不太方便。

    struct C objC;
    objC.objB.x = 10;
    ((struct B*)&objC)->x = 10;

要想訪問父類的成員x,有兩種方法,一種是objC.objB.x;另一種是((struct B*)&objC)->x = 10。這兩種方式都看起來不夠直接。而在子類方法中訪問父類成員是非常頻繁的。

void c_member_method(struct C* pObjC)
{
    pObjC->objB.x = 20; /* 訪問父類成員 */
    pObjC->f = 0.23f; /* 訪問自身成員 */
}

第一種方法,感覺更像是OB風格,而不是OO。
第二種方法,必須進行強制類型轉換,感覺語法上不夠美觀。

2.2 子類包含所有的父類成員

struct C
{
    int x;
    int y;
    int z;

    float f;
    char s[10];
};

把所有的父類成員原樣作為子類的成員。這樣子類對象訪問繼承來的成員就非常直接了。

void c_member_method(struct C* pObjC)
{
    pObjC->x = 20; /* 訪問父類成員 */
    pObjC->f = 0.23f; /* 訪問自身成員 */
}

void main()
{
    struct C objC;
    objC.x = 10;
}

看起來很好,實際上在工程上會存在一個很大的問題:難以維護!例如,每當創建一個子類,必須原樣書寫所有的父類成員,當父類定義變動時,子類需要做出同樣的修改。一旦父類稍具規模,維護這種繼承關系將是一場噩夢!

那麼如何解決的?
方法是現成的,那就是利用C語言的預處理宏定義#define. 如下所示:

#define B_STRUCT \
    int x; \
    int y; \
    int z

struct B
{
    B_STRUCT;
};

struct C
{
    B_STRUCT;
    float f;
    char s[10];
};

當繼承層級更深時,例如 C繼承B,D繼承C,可以照搬此方法。

#define B_STRUCT \
    int x; \
    int y; \
    int z

struct B
{
    B_STRUCT;
};

#define C_STRUCT \
    B_STRUCT; \
    float f; \
    char s[10]

struct C
{
    C_STRUCT;
};

#define D_STRUCT \
    C_STRUCT; \
    double d

struct D
{
    D_STRUCT;
};

通過宏定義,可以很容易實現和維護這種繼承關系。

3 方法(成員函數)的封裝與繼承

C語言中沒有成員函數的概念,語言本身也不支持。使用C語言實現真正的成員函數幾乎是不可能的,除非嵌入匯編語言。與其使用匯編語言,還不如直接使用C++呢。所以,我們不追求形式上的成員函數,只實現意義上的成員函數–(對給定類型對象進行操作)的函數,並使用帶結構名前綴的函數名加以命名之。我們還是以上面的例子進行說明。


typedef struct B B;

static void b_member_function(B* pobjB) /* 類B的成員函數 */
{

}

typedef struct C C;
static void c_member_function(C* pobjC) /* 類C的成員函數 */
{

}

對成員函數的調用,有兩種情形:(1)外部代碼調用成員函數;(2)子類成員函數中調用父類的成員函數;


static void c_member_function(C* pobjC)
{
    b_member_function((B*)pobjC); /* 子類成員函數內部調用父類成員函數 */
}
void main()
{
    C* pObjC = malloc(sizeof(C));
    b_member_function((B*)pObjC);  /* 外部代碼調用成員函數 */

    free(pObjC);
}

這兩種情況都需要對實參進行強制類型轉換為父類型。C編譯器對類型繼承關系一無所知,無法從語法上對繼承進行自動支持,所以只能手動強制類型轉換了。

有些人喜歡更進一步模擬成員函數,把所有成員函數的地址作為指針類型的成員變量存儲到結構體內部。如下:

#define B_STRUCT \
    int x; \
    int y; \
    int z; \
    void (*pb_member_function1)(B*); \
    void (*pb_member_function2)(B*, int arg)

struct B
{
    B_STRUCT;   
};

/* 初始化B對象的同時初始化 */
B* b = malloc(sizeof(B));
b->pb_member_function1 = b_member_function1;
b->pb_member_function2 = b_member_function2;

/* 調用 */
b->b_member_function1(b);

這樣形式上更加接近“成員函數”,但同時也帶來了額外的內存開銷和代碼量。為了減小內存消耗,有人提出不再在對象中完全存放所有成員函數指針,而是只存放一個指向成員函數地址列表的指針。畢竟,同一類型的所有實例(對象)共享相同的一組成員函數。

/* B類型的成員方法表 */
const struct B_MethodTable
{
    void (*pb_member_function1)(B*); 
    void (*pb_member_function2)(B*, int arg);
}b_method_table{
    b_member_function1,
    b_member_function2,
};

#define B_STRUCT \
    int x; \
    int y; \
    int z; \
    struct B_MethodTable * pMethodTable;

struct B
{
    B_STRUCT;   
};

/* 初始化B對象的同時初始化 */
B* b = malloc(sizeof(B));
b->pMethodTable = &b_method_table;

/* 調用 */
b->pMethodTable->pb_member_function1(b);

這樣在一定程度上減小了內存占用量和代碼量,但是隊成員函數的調用寫法卻變得非常繁瑣不自然。

4 做到何種程度?

使用C語言做OO開發時,要掌握好一個度。不要過分追求對OO語言C++的模擬,完全模擬C++的話,還不如干脆直接使用C++。

有些語言特性無法模擬,如C++中private,protected等訪問限定符,成員函數的this指針。更應該注重意義上的模擬,通過一些命名規則和約定來達到OO。 永遠不要違背C語言的設計哲學:程序員控制一切,直接簡明。

這個度的把握需要根據具體的項目規模和需求,是實踐中摸索出來的,無法給出理論上的最優值。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved