C語言程式的模組化——繼承(2)
在C語言程式的模組化——封裝中,介紹瞭如何使用C語言的結構體來實現一個類的封裝,並通過掩碼結構體的方式實
現了類成員的保護,使公有屬性和私有屬性共存。
現在再談談面向物件的另一個基本特性——繼承。
繼承表示類與類之間的層次關係,這種關係使得某類物件可以繼承另外一類物件的特徵和能力,繼承又可以分為單繼承和多繼承,單繼承是子類只從一個父類繼承,而多繼承中的子類可以從多於一個的父類繼承,Java是單繼承的語言,而C++允許多繼承。
談到繼承,不得不說說派生,其實繼承和派生是同一個動作的兩種不同角度的表述。
當繼承了一個基類而創造了一個新類時,派生的概念就誕生了,派生當然是從基類派生的,而派生出來的類當然繼承了基類的東西。
繼承和派生不是一對好基友,他們根本就是一個動作的兩種不同說法,強調動作的起始點的時候,我們說這是從某某類繼承來的;強調動作的終點時,我們說派生出了某某類,這個派生出來的類叫作基類或超類或父類的派生類或子類。
eg:
window_t tWin = new_window(); //!< 建立一個新的window物件
tWin.show(); //!< 顯示窗體
眾所周知,類總是會提供一些方法,可以讓我們很方便的使用,能夠實現這一技術的必要手段就是將函式指標一起封裝在結構體中。在C語言中,類的方法(method)是通過函式指標(或者函式指標的集合)——我們叫做虛擬函式(表)來實現的。虛擬函式表同樣可以單獨存在,我們稱之為interface。在C語言中,虛函書表是可以直接通過封裝了純函式指標的結構體來實現的。如下面的程式碼所示:
//! \name interface definition
//! @{
#define DEF_INTERFACE(__NAME,...) \
typedef struct __NAME __NAME;\
__VA_ARGS__\
struct __NAME {
#define END_DEF_INTERFACE(__NAME) \
};
//! @}
例如,我們可以使用上面的巨集來定義一個位元組流的讀寫介面:
DEF_INTERFACE(i_pipe_byte_t)
bool (*write)(uint8_t chByte);
bool (*read)(uint8_t *pchByte)
END_DEF_INTERFACE(i_pipe_byte_t)
這類介面非常適合定義一個模組的依賴型介面——比如,某一個數據幀解碼的模組是依賴於對位元組流的讀寫的,通過在該模組中使用這樣一個介面,
並通過專門的介面註冊函式,即可實現所謂的面向介面開發——將模組的邏輯實現與具體應用相關的資料流隔離開來。例如:
frame.c
...
DEF_CLASS(frame_t)
i_pipe_byte_t tStream; //!< 流介面
...
END_DEF_CLASS(frame_t)
//! 介面註冊函式
bool frame_register_stream_interface(frame_t *ptFrame, i_pipe_byte_t tStream)
{
//! 去除掩碼結構體的保護
CLASS(frame_t) *ptF = (CLASS(frame_t) *)ptFrame;
//! 合法性檢查
if (NULL == tStream.write || NULL == tStream.read || NULL == ptFrame ) {
return false;
}
ptF->tStream = tStream; //!< 設定介面
return true;
}
frame.h
...
EXTERN_CLASS(frame_t)
i_pipe_byte_t tStream; //!< 流介面
...
END_EXTERN_CLASS(frame_t)
//! 介面註冊函式
extern bool frame_register_stream_interface(frame_t *ptFrame, i_pipe_byte_t tStream);
extern bool frame_init(frame_t *ptFrame);
基於這樣的模組,一個可能的外部使用方法是這樣的:
app.c
...
static bool serial_out(uint8_t chByte)
{
...
}
static bool serial_in(uint8_t *pchByte)
{
...
}
static frame_t s_tFrame;
...
void app_init(void)
{
//! 初始化
frame_init(&s_tFrame);
//! 初始化介面
do {
i_pipe_byte_t tPipe = {&serial_out, &serial_in};
frame_register_stream_interface(&s_tFrame, tPipe);
} while(0);
}
像這個例子展示的這樣,將介面直接封裝在掩碼結構體中的形式,我們並不能將其稱為“實現(implement)了介面i_pipe_byte_t”,
這只是內部將虛擬函式(表)表作為了一個普通的成員而已,我們可以認為這是加入了private屬性的,可過載的內部成員函式。下面,我們將介紹如何真正的“實現(implement)”指定的介面。首先,我們要藉助下面的專門定義的巨集:
#define DECLARE_CLASS(__NAME) \
typedef union __NAME __NAME; \
#define __DEF_CLASS(__NAME,...) \
/*typedef union __NAME __NAME; */ \
typedef struct __##__NAME __##__NAME; \
struct __##__NAME { \
__VA_ARGS__
#define DEF_CLASS(__NAME, ...) __DEF_CLASS(__NAME, __VA_ARGS__)
#define __END_DEF_CLASS(__NAME, ...) \
}; \
union __NAME { \
__VA_ARGS__ \
uint_fast8_t __NAME##__chMask[(sizeof(__##__NAME) + sizeof(uint_fast8_t) - 1) / sizeof(uint_fast8_t)];
};
#define END_DEF_CLASS(__NAME, ...) __END_DEF_CLASS(__NAME, __VA_ARGS__)
#define __EXTERN_CLASS_OBJ( __TYPE, __OBJ ) \
extern union { \
CLASS(__TYPE) __##__OBJ; \
__TYPE __OBJ; \
};
#define EXTERN_CLASS_OBJ(__TYPE, __OBJ) \
__EXTERN_CLASS_OBJ( __TYPE, __OBJ )
#define __EXTERN_CLASS(__NAME,...) \
/*typedef union __NAME __NAME; */ \
union __NAME { \
__VA_ARGS__ \
uint_fast8_t __NAME##__chMask[(sizeof(struct{\
__VA_ARGS__
#define EXTERN_CLASS(__NAME, ...) __EXTERN_CLASS(__NAME, __VA_ARGS__)
#define END_EXTERN_CLASS(__NAME, ...) \
}) + sizeof(uint_fast8_t) - 1) / sizeof(uint_fast8_t)]; \
};
為了很好的說明上面巨集的用法,我們以一個比較具體的例子來示範一下。這是一個通用的序列裝置驅動的例子,這個例子的
意圖是,為所有的類似USART,I2C,SPI這樣的序列資料介面建立一個基類,隨後,不同的外設都從該基類繼承並派生出
屬於自己的基類,比如USART類等等——這種方法是面向物件開發尤其是面向介面開發中非常典型的例子。首先,我們要
定義一個高度抽象的介面,該介面描述了我們是期待如何最簡單的使用一個序列裝置的,同時一起定義實現了該類的基類
serial_dev_t:
serial_device.h
DECLARE_CLASS( serial_dev_t );
//! 這是我們定義的介面i_serial_t 這裡的語法看起來似乎有點怪異,後面將介紹
DEF_INTERFACE( i_serial_t)
fsm_rt_t (*write)(serial_dev_t *ptDev, uint8_t *pchStream, uint_fast16_t hwSize); //!< i_serial_t 介面的write方法
fsm_rt_t (*read)(serial_dev_t *ptDev, uint8_t *pchStream, uint_fast16_t hwSize); //!< i_serial_t 介面的read方法
END_DEF_INTERFACE( i_serial_t )
//! 這是一個實現了介面i_serial_t的基類serial_dev_t
EXTERN_CLASS( serial_dev_t, IMPLEMENT(i_serial_t) )
//! 類serial_dev_t的內部定義
...
END_EXTERN_CLASS_IMPLEMENT( serial_dev_t, IMPLEMENT(i_serial_t))
如果不仔細看,這個例子似乎比較清楚了,一個基類serial_dev_t實現了介面i_serial_t。
註釋:
值得注意的是這裡有一個向前引用的問題,也就是 i_serial_t 使用到了還未定義的 serial_dev_t
如果你曾經定義過類似下面的結構體,你就知道蹊蹺在哪裡了,同時,你也就知道解決的
原理了:
//! 一個無法編譯通過的寫法
typedef struct {
....
item_t *ptNext;
}item_t;
等效的正確寫法如下:
//! 前置宣告的例子
typedef struct item_t item_t;
struct item_t {
...
item_t *ptNext;
};
可見,前置宣告是解決這類問題的關鍵,這裡,下面的巨集值得注意:
#define DECLARE_CLASS(__NAME) \
typedef union __NAME __NAME;
以此為例,我來演示一下如何用引數巨集實現方便的前置宣告:
#define DEF_FORWARD_LIST(__NAME) \
typedef struct __NAME __NAME;\
struct __NAME {
#define END_DEF_FORWARD_LIST(__NAME) \
};
使用的時候這樣
DEF_FORWARD_LIST(item_t)
...
item_t *ptNext;
END_DEF_FORWARD_LIST(item_t)
這隻解決了一個疑惑,另外一個疑惑就是為什麼可以在引數巨集裡面插入另外一段程式碼?答案是一直可以,比如,我常這麼幹:
# define SAFE_ATOM_CODE(...) {\
istate_t tState = GET_GLOBAL_INTERRUPT_STATE();\
DISABLE_GLOBAL_INTERRUPT();\
__VA_ARGS__;\
SET_GLOBAL_INTERRUPT_STATE(tState);\
}
這是原子操作的巨集,使用的時候,只要在"..."的位置寫程式就好了,例如:
adc.c
...
static volatile uint16_t s_hwADCResult;
...
ISR(ADC_vect)
{
//! 獲取ADC的值
s_hwADCResult = ADC0;
}
//! \brief 帶原子保護的adc結果讀取
uint16_t get_adc_result(void)
{
uint16_t hwResult;
SAFE_ATOM_CODE(
hwResult = s_hwResult;
)
return hwResult;
}
adc.h
...
//! 可以隨時安全的讀取ADC的結果
extern uint16_t get_adc_result(void);
...
現在看來在引數巨集裡面插入大段大段的程式碼根本不是問題。
在看程式碼,對一個類來說,是否實現介面,以及實現幾個介面其實是不確定的,例如這個例子中我們實現了一個介面:
EXTERN_CLASS(example_t, IMPLEMENT( i_serial_t ))
....
END_EXTERN_CLASS(example_t, IMPLEMENT( i_serial_t ) )
那麼如何在DEF_CLASS和EXTERN_CLASS中體現這種對不確定性的支援呢?
顯然,這時候變長引數就成了關鍵,幸好C99位我們提供了這個便利,直接在引數巨集裡面加入“...”在巨集本體裡面用
__VA_ARGS__就可以代表“...”的內容了。
經過這樣的解釋,回頭再去看前面的類定義,根本不算什麼。^_^
那麼一個類實現(implement)了某個介面,這有神馬意義呢? 意義如下,我們就可以像正常類那麼使用介面提供的方法了:
//! 假設我們獲取了一個名叫“usart0”的序列裝置
serial_dev_t *ptDev = get_serial_device("usart0");
uint8_t chString[] = "Hello World!";
//! 我們就可以訪問這個物件的方法,比如傳送字串
while ( fsm_rt_cpl !=
ptDev->write(ptDev, chString, sizeof(chString))
);
//! 當然這個物件仍然是被掩碼結構體保護的,因為ptDev的另外一個可見的成員是ptDev->chMask,你懂的
接下來,我們要處理的問題就是繼承和派生……唉,繞了這麼大的圈子,才切入本文的重點。記得有個諺語的全文叫做“博士賣驢,下筆千言,離題萬里,未有驢子……”
要實現繼承和派生,只要藉助下面這個裝模作樣的巨集就可以了。
//! \brief macro for inheritance
#define INHERIT_EX(__TYPE, __NAME) \
union { \
__TYPE __NAME; \
__TYPE; \
};
/*! \note When deriving a new class from a base class, you should use INHERIT
* other than IMPLEMENT, although they looks the same now.
*/
#define __INHERIT(__TYPE) INHERIT_EX(__TYPE, base__##__TYPE)
#define INHERIT(__TYPE) __INHERIT(__TYPE)
/*! \note You can only use IMPLEMENT when defining INTERFACE. For Implement
* interface when defining CLASS, you should use DEF_CLASS_IMPLEMENT
* instead.
*/
#define __IMPLEMENT(__INTERFACE) INHERIT_EX(__INTERFACE, base__##__INTERFACE)
#define IMPLEMENT(__INTERFACE) __IMPLEMENT(__INTERFACE)
是的,它不過是把基類作為新類(結構體)的第一個元素,並起了一個好聽的名字叫base。“尼瑪坑爹了吧?”沒錯,其實就是這樣,沒什麼複雜的,所以我們可以很容易的從serial_dev_t繼承併為usart派生一個類出來:
usart.h
#include "serial_device.h"
...
EXTERN_CLASS(usart_t, INHERIT(serial_dev_t) )
uint8_t chName[20]; //!< 儲存名字,比如"USART0"
usart_reg_t *ptRegisters; //!< 指向裝置暫存器
...
END_EXTERN_CLASS(usart_t , INHERIT(serial_dev_t))
//! \brief 當然要提供一個函式來返回基類咯
extern serial_dev_t *usart_get_base(usart_t *ptUSART);
完成了這些,關於OOC格式上的表面工作,基本上就介紹完畢了。格式畢竟是表面工作,學會這些並不能讓你的程式碼面向物件,最多時看起來很高檔。真正關鍵的是給自己建立面向物件的思維模式和訓練自己相應的開發方法,這就需要你去看那些介紹面向物件方法的書了,比如面向物件的思想啊,設計模式阿,UML建模阿。還是那句老話,如果你不知道怎麼入門,看《UML+OOPC》。