C语言类的继承和派生

模块的封装(一):C语言类的封装 中,我们介绍了如何使用C语言的结构体来实现一个类的封装,并通过掩码结构体的方式实
现了类成员的保护。这一部分,我们将在此基础上介绍C语言类的继承和派生。其实继承和派生是同一个动作的两种不同角度的表述。
当我们继承了一个基类而创造了一个新类时,派生的概念就诞生了。派生当然是从基类派生的。派生出来的类当然继承了基类的东西。
继承和派生不是一对好基友,他们根本就是一个动作的两种不同说法,强调动作的起始点的时候,我们说这是从某某类继承来的;强调
动作的终点时,我们说派生出了某某类。——呼……真累……厄……不是阿……我不是唐僧。

      我们知道,类总是会提供一些方法,可以让我们很方便的使用,比如

  1.     window_t tWin = new_window();    //!< 创建一个新的window对象
  2.     tWin.show();                                 //!< 显示窗体
复制代码
显然,能够实现这一技术的必要手段就是将函数指针一起封装在结构体中。在C语言中,类的方法(method)是通过函数指针(或者函
数指针的集合)——我们叫做虚函数(表)来实现的。虚函数表同样可以单独存在,我们称之为interface。在C语言中,虚函书表是可以
直接通过封装了纯函数指针的结构体来实现的。如下面的代码所示:
  1. //! \name interface definition
  2. //! @{
  3. #define DEF_INTERFACE(__NAME,...)   \
  4.             typedef struct __NAME __NAME;\
  5.             __VA_ARGS__\
  6.             struct __NAME {

  7. #define END_DEF_INTERFACE(__NAME)   \
  8.             };
  9. //! @}
复制代码
例如,我们可以使用上面的宏来定义一个字节流的读写接口:

  1. DEF_INTERFACE(i_pipe_byte_t)
  2.     bool (*write)(uint8_t chByte);
  3.     bool (*read)(uint8_t *pchByte)
  4. END_DEF_INTERFACE(i_pipe_byte_t)
复制代码
这类接口非常适合定义一个模块的依赖型接口——比如,某一个数据帧解码的模块是依赖于对字节流的读写的,通过在该模块中使用这样一个接口,
并通过专门的接口注册函数,即可实现所谓的面向接口开发——将模块的逻辑实现与具体应用相关的数据流隔离开来。例如:

frame.c

  1. ...
  2. DEF_CLASS(frame_t)
  3.     i_pipe_byte_t tStream;     //!< 流接口
  4.     ...
  5. END_DEF_CLASS(frame_t)

  6. //! 接口注册函数
  7. bool frame_register_stream_interface(frame_t *ptFrame, i_pipe_byte_t tStream)
  8. {
  9.     //! 去除掩码结构体的保护
  10.     CLASS(frame_t) *ptF = (CLASS(frame_t) *)ptFrame;
  11.     //! 合法性检查
  12.     if (NULL == tStream.write || NULL == tStream.read || NULL == ptFrame ) {
  13.         return false;
  14.     }
  15.     ptF->tStream = tStream;    //!< 设置接口
  16.     return true;
  17. }
复制代码
frame.h

  1. ...
  2. EXTERN_CLASS(frame_t)
  3.     i_pipe_byte_t tStream;     //!< 流接口
  4.     ...
  5. END_EXTERN_CLASS(frame_t)

  6. //! 接口注册函数
  7. extern bool frame_register_stream_interface(frame_t *ptFrame, i_pipe_byte_t tStream);

  8. extern bool frame_init(frame_t *ptFrame);

复制代码
基于这样的模块,一个可能的外部使用方法是这样的:
app.c

  1. ...
  2. static bool serial_out(uint8_t chByte)
  3. {
  4.     ...
  5. }

  6. static bool serial_in(uint8_t *pchByte)
  7. {
  8.     ...
  9. }

  10. static frame_t s_tFrame;
  11. ...
  12. void app_init(void)
  13. {
  14.     //! 初始化
  15.     frame_init(&s_tFrame);    

  16.     //! 初始化接口
  17.     do {
  18.         i_pipe_byte_t tPipe = {&serial_out, &serial_in};
  19.         frame_register_stream_interface(&s_tFrame, tPipe);
  20.     } while(0);
  21. }
复制代码
像这个例子展示的这样,将接口直接封装在掩码结构体中的形式,我们并不能将其称为“实现(implement)了接口i_pipe_byte_t”,
这只是内部将虚函数(表)表作为了一个普通的成员而已,我们可以认为这是加入了private属性的,可重载的内部成员函数。下面,我们
将来介绍如何真正的“实现(implement)”指定的接口。首先,我们要借助下面的专门定义的宏:

  1. #define DEF_CLASS_IMPLEMENT(__NAME,__INTERFACE,...)\
  2.     typedef union __NAME __NAME;\
  3.     __VA_ARGS__\
  4.     typedef struct __##__NAME __##__NAME;\
  5.     struct __##__NAME {\
  6.         const __INTERFACE method;

  7. #define END_DEF_CLASS_IMPLEMENT(__NAME,__INTERFACE)\
  8.     };\
  9.     union __NAME {\
  10.         const __INTERFACE method;\
  11.         uint_fast8_t chMask[(sizeof(__##__NAME) + sizeof(uint_fast8_t) - 1) / sizeof(uint_fast8_t)];\
  12.     };

  13. #define EXTERN_CLASS_IMPLEMENT(__NAME,__INTERFACE,...) \
  14.     typedef union __NAME __NAME;\
  15.     __VA_ARGS__\
  16.     union __NAME {\
  17.         const __INTERFACE method;\
  18.         uint_fast8_t chMask[(sizeof(struct {\
  19.             const __INTERFACE method;

  20. #define END_EXTERN_CLASS_IMPLEMENT(__NAME, __INTERFACE) \
  21.         }) + sizeof(uint_fast8_t) - 1) / sizeof(uint_fast8_t)];\
  22.     };

复制代码
为了很好的说明上面宏的用法,我们以一个比较具体的例子来示范一下。这是一个通用的串行设备驱动的例子,这个例子的
意图是,为所有的类似USART,I2C,SPI这样的串行数据接口建立一个基类,随后,不同的外设都从该基类继承并派生出
属于自己的基类,比如USART类等等——这种方法是面向对象开发尤其是面向接口开发中非常典型的例子。首先,我们要
定义一个高度抽象的接口,该接口描述了我们是期待如何最简单的使用一个串行设备的,同时一起定义实现了该类的基类
serial_dev_t:
serial_device.h

  1. //! 这是一个实现了接口i_serial_t的基类serial_dev_t
  2. EXTERN_CLASS_IMPLEMENT( serial_dev_t, i_serial_t,

  3.         //! 这是我们定义的接口i_serial_t 这里的语法看起来似乎有点怪异,后面将介绍
  4.         DEF_INTERFACE( i_serial_t)     
  5.             fsm_rt_t (*write)(serial_dev_t *ptDev, uint8_t *pchStream, uint_fast16_t hwSize);    //!< i_serial_t 接口的write方法
  6.             fsm_rt_t (*read)(serial_dev_t *ptDev, uint8_t *pchStream, uint_fast16_t hwSize);     //!< i_serial_t 接口的read方法
  7.         END_DEF_INTERFACE( i_serial_t )
  8.     )
  9.     //! 类serial_dev_t的内部定义
  10.     ...
  11. END_EXTERN_CLASS_IMPLEMENT( serial_dev_t, i_serial_t )
复制代码
如果不仔细看,这个例子似乎比较清楚了,一个基类serial_dev_t实现了接口i_serial_t。但仔细一看这里不光语法奇怪,
而且还有很多细节。
首先,接口居然是定义在类的定义里面的,而且是定义在参数宏EXTERN_CLASS_IMPLEMENT里面的!
其次,似乎类serial_dev_t在接口i_serial_t定义之前就已经能implement它了,而且接口i_serial_t也反过来在自己的定义
中引用了基类serial_dev_t。如果你曾经定义过类似下面的结构体,你就知道蹊跷在哪里了,同时,你也就知道解决的
原理了:

  1. //! 一个无法编译通过的写法
  2. typedef struct {
  3.     ....
  4.     item_t *ptNext;
  5. }item_t;
复制代码
等效的正确写法如下:

  1. //! 前置声明的例子
  2. typedef struct item_t item_t;
  3. struct item_t {
  4.     ...
  5.     item_t *ptNext;
  6. };
复制代码
可见,前置声明是解决这类问题的关键,回头再看看EXTERN_CLASS_IMPLEMENT的宏,你就会看到前置声明的结构。
以此为例,我来演示一下如何用参数宏实现方便的前置声明:

  1. #define DEF_FORWARD_LIST(__NAME)    \
  2.     typedef struct __NAME __NAME;\
  3.     struct __NAME {

  4. #define END_DEF_FORWARD_LIST(__NAME)  \
  5.     };
复制代码
使用的时候这样

  1. DEF_FORWARD_LIST(item_t)
  2.     ...
  3.     item_t *ptNext;
  4. END_DEF_FORWARD_LIST(item_t)
复制代码
这只解决了一个疑惑,另外一个疑惑就是为什么可以在参数宏里面插入另外一段代码?答案是一直可以,比如,我常这么干:

  1. # define SAFE_ATOM_CODE(__CODE)     {\
  2.         istate_t tState = GET_GLOBAL_INTERRUPT_STATE();\
  3.         DISABLE_GLOBAL_INTERRUPT();\
  4.         __CODE;\
  5.         SET_GLOBAL_INTERRUPT_STATE(tState);\
  6.     }
复制代码
这是原子操作的宏,使用的时候,只要在__CODE的位置写程序就好了,例如:
adc.c

  1. ...
  2. static volatile uint16_t s_hwADCResult;
  3. ...
  4. ISR(ADC_vect)
  5. {
  6.     //! 获取ADC的值
  7.     s_hwADCResult = ADC0;
  8. }

  9. //! \brief 带原子保护的adc结果读取
  10. uint16_t get_adc_result(void) 
  11. {
  12.     uint16_t hwResult;
  13.     SAFE_ATOM_CODE(
  14.         hwResult = s_hwResult;
  15.     )
  16.     return hwResult;
  17. }
复制代码
adc.h

  1. ...
  2. //! 可以随时安全的读取ADC的结果
  3. extern uint16_t get_adc_result(void);
  4. ...
复制代码
现在看来在参数宏里面插入大段大段的代码根本不是问题,问题是,当我不想插入的时候怎么办呢?例如这个例子里面,宏
EXTERN_CLASS_IMPLEMENT(__NAME, __INTERFACE,...)这里,我们真正关心的是__NAME和__INTERFACE,而是否插入
其它代码到定义结构里面是不确定的,我们很可能就直接想这么用

  1. EXTERN_CLASS_IMPLEMENT(example_t, i_serial_t)
  2. ....
  3. END_EXTERN_CLASS_IMPLEMENT(example_t, i_serial_t)
复制代码
显然,这时候变长参数就成了关键,幸好C99位我们提供了这个便利,直接在参数宏里面加入“...”在宏本体里面用
__VA_ARGS__就可以代表“...”的内容了。

经过这样的解释,回头再去看前面的类定义,根本不算什么。^_^
那么一个类实现(implement)了某个接口,这有神马意义呢? 意义如下,我们就可以像正常类那么使用接口提供的方法了:

  1. //! 假设我们获取了一个名叫“usart0”的串行设备
  2. serial_dev_t *ptDev = get_serial_device("usart0");

  3. uint8_t chString[] = "Hello World!";

  4. //! 我们就可以访问这个对象的方法,比如发送字符串
  5. while ( fsm_rt_cpl != 
  6.     ptDev->method.write(ptDev, chString, sizeof(chString))
  7. );
  8. //! 当然这个对象仍然是被掩码结构体保护的,因为ptDev的另外一个可见的成员是ptDev->chMask,你懂的
复制代码
接下来,我们要处理的问题就是继承和派生……唉,绕了这么大的圈子,才切入本文的重点。记得有个谚语的全文叫做“博士卖驴,
下笔千言,离题万里,未有驴子……”
要实现继承和派生,只要借助下面这个装模作样的宏就可以了。

//! \brief macro for inheritance
#define INHERIT(__TYPE)             __TYPE base;
[/code]是的,它不过是把基类作为新类(结构体)的第一个元素,并起了一个好听的名字叫base。“尼玛坑爹了吧?”没错,其实就是这
样,没什么复杂的,所以我们可以很容易的从serial_dev_t继承并为usart派生一个类出来:
usart.h

  1. #include "serial_device.h"
  2. ... 
  3. EXTERN_CLASS(usart_t) INHERIT(serial_dev_t)

  4.     uint8_t chName[20];                        //!< 保存名字,比如"USART0"
  5.     usart_reg_t *ptRegisters;                  //!< 指向设备寄存器
  6.     ...

  7. END_EXTERN_CLASS(usart_t)

  8. //! \brief 当然要提供一个函数来返回基类咯
  9. extern serial_dev_t *usart_get_base(usart_t *ptUSART);
复制代码
完成了这些,关于OOC格式上的表面工作,基本上就介绍完毕了。格式毕竟是表面工作,学会这些并不能让你的代码面向对象,
最多时看起来很高档。真正关键的是给自己建立面向对象的思维模式和训练自己相应的开发方法,这就需要你去看那些介绍面向
对象方法的书了,比如面向对象的思想啊,设计模式阿,UML建模阿。还是那句老话,如果你不知道怎么入门,看《UML+OOPC》。

打完手工,谢谢~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值