在
模块的封装(一):C语言类的封装
中,我们介绍了如何使用C语言的结构体来实现一个类的封装,并通过掩码结构体的方式实
现了类成员的保护。这一部分,我们将在此基础上介绍C语言类的继承和派生。其实继承和派生是同一个动作的两种不同角度的表述。
当我们继承了一个基类而创造了一个新类时,派生的概念就诞生了。派生当然是从基类派生的。派生出来的类当然继承了基类的东西。
继承和派生不是一对好基友,他们根本就是一个动作的两种不同说法,强调动作的起始点的时候,我们说这是从某某类继承来的;强调
动作的终点时,我们说派生出了某某类。——呼……真累……厄……不是阿……我不是唐僧。
我们知道,类总是会提供一些方法,可以让我们很方便的使用,比如 显然,能够实现这一技术的必要手段就是将函数指针一起封装在结构体中。在C语言中,类的方法(method)是通过函数指针(或者函
数指针的集合)——我们叫做虚函数(表)来实现的。虚函数表同样可以单独存在,我们称之为interface。在C语言中,虚函书表是可以
直接通过封装了纯函数指针的结构体来实现的。如下面的代码所示: 例如,我们可以使用上面的宏来定义一个字节流的读写接口: 这类接口非常适合定义一个模块的依赖型接口——比如,某一个数据帧解码的模块是依赖于对字节流的读写的,通过在该模块中使用这样一个接口,
并通过专门的接口注册函数,即可实现所谓的面向接口开发——将模块的逻辑实现与具体应用相关的数据流隔离开来。例如:
frame.c frame.h 基于这样的模块,一个可能的外部使用方法是这样的:
app.c 像这个例子展示的这样,将接口直接封装在掩码结构体中的形式,我们并不能将其称为“实现(implement)了接口i_pipe_byte_t”,
这只是内部将虚函数(表)表作为了一个普通的成员而已,我们可以认为这是加入了private属性的,可重载的内部成员函数。下面,我们
将来介绍如何真正的“实现(implement)”指定的接口。首先,我们要借助下面的专门定义的宏: 为了很好的说明上面宏的用法,我们以一个比较具体的例子来示范一下。这是一个通用的串行设备驱动的例子,这个例子的
意图是,为所有的类似USART,I2C,SPI这样的串行数据接口建立一个基类,随后,不同的外设都从该基类继承并派生出
属于自己的基类,比如USART类等等——这种方法是面向对象开发尤其是面向接口开发中非常典型的例子。首先,我们要
定义一个高度抽象的接口,该接口描述了我们是期待如何最简单的使用一个串行设备的,同时一起定义实现了该类的基类
serial_dev_t:
serial_device.h 如果不仔细看,这个例子似乎比较清楚了,一个基类serial_dev_t实现了接口i_serial_t。但仔细一看这里不光语法奇怪,
而且还有很多细节。
首先,接口居然是定义在类的定义里面的,而且是定义在参数宏EXTERN_CLASS_IMPLEMENT里面的!
其次,似乎类serial_dev_t在接口i_serial_t定义之前就已经能implement它了,而且接口i_serial_t也反过来在自己的定义
中引用了基类serial_dev_t。如果你曾经定义过类似下面的结构体,你就知道蹊跷在哪里了,同时,你也就知道解决的
原理了: 等效的正确写法如下: 可见,前置声明是解决这类问题的关键,回头再看看EXTERN_CLASS_IMPLEMENT的宏,你就会看到前置声明的结构。
以此为例,我来演示一下如何用参数宏实现方便的前置声明: 使用的时候这样 这只解决了一个疑惑,另外一个疑惑就是为什么可以在参数宏里面插入另外一段代码?答案是一直可以,比如,我常这么干: 这是原子操作的宏,使用的时候,只要在__CODE的位置写程序就好了,例如:
adc.c adc.h 现在看来在参数宏里面插入大段大段的代码根本不是问题,问题是,当我不想插入的时候怎么办呢?例如这个例子里面,宏
EXTERN_CLASS_IMPLEMENT(__NAME, __INTERFACE,...)这里,我们真正关心的是__NAME和__INTERFACE,而是否插入
其它代码到定义结构里面是不确定的,我们很可能就直接想这么用 显然,这时候变长参数就成了关键,幸好C99位我们提供了这个便利,直接在参数宏里面加入“...”在宏本体里面用
__VA_ARGS__就可以代表“...”的内容了。
经过这样的解释,回头再去看前面的类定义,根本不算什么。^_^
那么一个类实现(implement)了某个接口,这有神马意义呢? 意义如下,我们就可以像正常类那么使用接口提供的方法了: 接下来,我们要处理的问题就是继承和派生……唉,绕了这么大的圈子,才切入本文的重点。记得有个谚语的全文叫做“博士卖驴,
下笔千言,离题万里,未有驴子……”
要实现继承和派生,只要借助下面这个装模作样的宏就可以了。
//! \brief macro for inheritance
#define INHERIT(__TYPE) __TYPE base;
[/code]是的,它不过是把基类作为新类(结构体)的第一个元素,并起了一个好听的名字叫base。“尼玛坑爹了吧?”没错,其实就是这
样,没什么复杂的,所以我们可以很容易的从serial_dev_t继承并为usart派生一个类出来:
usart.h 完成了这些,关于OOC格式上的表面工作,基本上就介绍完毕了。格式毕竟是表面工作,学会这些并不能让你的代码面向对象,
最多时看起来很高档。真正关键的是给自己建立面向对象的思维模式和训练自己相应的开发方法,这就需要你去看那些介绍面向
对象方法的书了,比如面向对象的思想啊,设计模式阿,UML建模阿。还是那句老话,如果你不知道怎么入门,看《UML+OOPC》。
打完手工,谢谢~
现了类成员的保护。这一部分,我们将在此基础上介绍C语言类的继承和派生。其实继承和派生是同一个动作的两种不同角度的表述。
当我们继承了一个基类而创造了一个新类时,派生的概念就诞生了。派生当然是从基类派生的。派生出来的类当然继承了基类的东西。
继承和派生不是一对好基友,他们根本就是一个动作的两种不同说法,强调动作的起始点的时候,我们说这是从某某类继承来的;强调
动作的终点时,我们说派生出了某某类。——呼……真累……厄……不是阿……我不是唐僧。
我们知道,类总是会提供一些方法,可以让我们很方便的使用,比如 显然,能够实现这一技术的必要手段就是将函数指针一起封装在结构体中。在C语言中,类的方法(method)是通过函数指针(或者函
数指针的集合)——我们叫做虚函数(表)来实现的。虚函数表同样可以单独存在,我们称之为interface。在C语言中,虚函书表是可以
直接通过封装了纯函数指针的结构体来实现的。如下面的代码所示: 例如,我们可以使用上面的宏来定义一个字节流的读写接口: 这类接口非常适合定义一个模块的依赖型接口——比如,某一个数据帧解码的模块是依赖于对字节流的读写的,通过在该模块中使用这样一个接口,
并通过专门的接口注册函数,即可实现所谓的面向接口开发——将模块的逻辑实现与具体应用相关的数据流隔离开来。例如:
frame.c frame.h 基于这样的模块,一个可能的外部使用方法是这样的:
app.c 像这个例子展示的这样,将接口直接封装在掩码结构体中的形式,我们并不能将其称为“实现(implement)了接口i_pipe_byte_t”,
这只是内部将虚函数(表)表作为了一个普通的成员而已,我们可以认为这是加入了private属性的,可重载的内部成员函数。下面,我们
将来介绍如何真正的“实现(implement)”指定的接口。首先,我们要借助下面的专门定义的宏: 为了很好的说明上面宏的用法,我们以一个比较具体的例子来示范一下。这是一个通用的串行设备驱动的例子,这个例子的
意图是,为所有的类似USART,I2C,SPI这样的串行数据接口建立一个基类,随后,不同的外设都从该基类继承并派生出
属于自己的基类,比如USART类等等——这种方法是面向对象开发尤其是面向接口开发中非常典型的例子。首先,我们要
定义一个高度抽象的接口,该接口描述了我们是期待如何最简单的使用一个串行设备的,同时一起定义实现了该类的基类
serial_dev_t:
serial_device.h 如果不仔细看,这个例子似乎比较清楚了,一个基类serial_dev_t实现了接口i_serial_t。但仔细一看这里不光语法奇怪,
而且还有很多细节。
首先,接口居然是定义在类的定义里面的,而且是定义在参数宏EXTERN_CLASS_IMPLEMENT里面的!
其次,似乎类serial_dev_t在接口i_serial_t定义之前就已经能implement它了,而且接口i_serial_t也反过来在自己的定义
中引用了基类serial_dev_t。如果你曾经定义过类似下面的结构体,你就知道蹊跷在哪里了,同时,你也就知道解决的
原理了: 等效的正确写法如下: 可见,前置声明是解决这类问题的关键,回头再看看EXTERN_CLASS_IMPLEMENT的宏,你就会看到前置声明的结构。
以此为例,我来演示一下如何用参数宏实现方便的前置声明: 使用的时候这样 这只解决了一个疑惑,另外一个疑惑就是为什么可以在参数宏里面插入另外一段代码?答案是一直可以,比如,我常这么干: 这是原子操作的宏,使用的时候,只要在__CODE的位置写程序就好了,例如:
adc.c adc.h 现在看来在参数宏里面插入大段大段的代码根本不是问题,问题是,当我不想插入的时候怎么办呢?例如这个例子里面,宏
EXTERN_CLASS_IMPLEMENT(__NAME, __INTERFACE,...)这里,我们真正关心的是__NAME和__INTERFACE,而是否插入
其它代码到定义结构里面是不确定的,我们很可能就直接想这么用 显然,这时候变长参数就成了关键,幸好C99位我们提供了这个便利,直接在参数宏里面加入“...”在宏本体里面用
__VA_ARGS__就可以代表“...”的内容了。
经过这样的解释,回头再去看前面的类定义,根本不算什么。^_^
那么一个类实现(implement)了某个接口,这有神马意义呢? 意义如下,我们就可以像正常类那么使用接口提供的方法了: 接下来,我们要处理的问题就是继承和派生……唉,绕了这么大的圈子,才切入本文的重点。记得有个谚语的全文叫做“博士卖驴,
下笔千言,离题万里,未有驴子……”
要实现继承和派生,只要借助下面这个装模作样的宏就可以了。
//! \brief macro for inheritance
#define INHERIT(__TYPE) __TYPE base;
[/code]是的,它不过是把基类作为新类(结构体)的第一个元素,并起了一个好听的名字叫base。“尼玛坑爹了吧?”没错,其实就是这
样,没什么复杂的,所以我们可以很容易的从serial_dev_t继承并为usart派生一个类出来:
usart.h 完成了这些,关于OOC格式上的表面工作,基本上就介绍完毕了。格式毕竟是表面工作,学会这些并不能让你的代码面向对象,
最多时看起来很高档。真正关键的是给自己建立面向对象的思维模式和训练自己相应的开发方法,这就需要你去看那些介绍面向
对象方法的书了,比如面向对象的思想啊,设计模式阿,UML建模阿。还是那句老话,如果你不知道怎么入门,看《UML+OOPC》。
打完手工,谢谢~