文章转载来源:[交流][微知识]模块的封装(二):C语言类的继承和派生
在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》。
同系列目录: