[交流][微知识]模块的封装(二):C语言的继承和派生


  在模块的封装(一):C语言的封装中,我们介绍了如何使用C语言的结构体来实现一个类的封装,并通过掩码结构体的方式实

现了类成员的保护。这一部分,我们将 在此的基础上介绍C语言类的继承和派生。其实继承和派生是一个动作的两种不同角度的表达

。当我们继承了一个基类而创造了一个新类时,派生的概念就诞生了。派生当然是从基类派生的。派生出来的类当然是继承了基类的

东西。继承和派生不是一对好基友,他们根本就是一个动作的两种不同的说法,强调动作的起始点的时候,我们说这是从某某类继承

来的,强调动作的终点时,我们说派生出了某某类,

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

     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();
}

像这个例子展示的这样,将接口直接封装在掩码结构体的形式,我们并不能将其称为“实现(implement)

了接口i_pipe_byte_t”,这只是内部将虚函数表作为了一个普通成员而已,我们可以认为这是加入了private

属性的,可重载的内部成员函数。下面我们将来介绍如何真正的实现(implement)指定的接口。首先,我

们要借助下面定义的宏

 #define DEF_CLASS_IMPLEMENT(__NAME,__INTERFACE,...)\
typedef union __NAME __NAME;\
__VA_ARGS__\
typedef struct __##__NAME __##__NAME;\
struct __##__NAME {\
const __INTERFACE method; #define END_DEF_CLASS_IMPLEMENT(__NAME,__INTERFACE)\
};\
union __NAME {\
const __INTERFACE method;\
uint_fast8_t chMask[(sizeof(__##__NAME) + sizeof(uint_fast8_t) - ) / sizeof(uint_fast8_t)];\
}; #define EXTERN_CLASS_IMPLEMENT(__NAME,__INTERFACE,...) \
typedef union __NAME __NAME;\
__VA_ARGS__\
union __NAME {\
const __INTERFACE method;\
uint_fast8_t chMask[(sizeof(struct {\
const __INTERFACE method; #define END_EXTERN_CLASS_IMPLEMENT(__NAME, __INTERFACE) \
}) + sizeof(uint_fast8_t) - ) / sizeof(uint_fast8_t)];\
};

为了很好的说明上面宏的用法,我们以一个比较具体的例子来示范一下。这是一个通用的串行设备

驱动的例子,这个例子的意图是,为所有的类似USART,I2C,SPI这样的串行数据接口建立一个

基类,随后不同的外设都从该基类继承并派生出属于自己的基类,比如USART类等----这种方法

是面向对象开发尤其是面向接口开发中非常典型的例子。首先,我们要定义一个高度抽象的接口,

该接口描述了我们是期待如何最简单的使用一个串行设备的,同时一起定义实现了该类的基类

serial_dev_t;

serial_device.h

 //! 这是一个实现了接口i_serial_t的基类serial_dev_t
EXTERN_CLASS_IMPLEMENT( serial_dev_t, i_serial_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 )
)
//! 类serial_dev_t的内部定义
...
END_EXTERN_CLASS_IMPLEMENT( serial_dev_t, i_serial_t )

如果不仔细看,这个例子似乎比较清楚了,一个基类serial_dev_t实现了接口i_serial_t.但仔细一看

这里不光语法奇怪,而且含有很多细节。

首先,接口居然是定义在类里面的,而且是定义在参数宏EXTERN_CLASS_IMPEMENT里面的。

其次,似乎类serial_dev_t在接口i_serial_t定义之前就已经能implement他了,而接口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;
};

可见前置声明是解决这类问题的关键,回头看EXTERN_CLASS_IMPLEMENT的宏,你就会看到前置

声明的结构。以此为例,我来演示一下如何使用参数宏实现方便的前置声明:

 #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_IMPLEMENT(_NAME,INTEFACE,...)这里我们真正关心的是 _NAME 和 INTEFACE,而是否插入

其他代码定义结构体里面是不确定的,我们很可能就这么用

 EXTERN_CLASS_IMPLEMENT(example_t, i_serial_t)
....
END_EXTERN_CLASS_IMPLEMENT(example_t, i_serial_t)

显然这时候变长参数就成了关键,幸好C99为我们提供了这个便利,直接在参数宏里面加入“...”在宏本体里面用

_VA_ARGS_就可以代表"..."的内容。

经过这样的介绍,回头看看前面类的定义,根本不算什么。

那么一个类实现(implement)了某个接口,这有神马意义呢?意义如下,我们就可以像正常类那么使用接口提

供的方法了:

 //! 假设我们获取了一个名叫“usart0”的串行设备
serial_dev_t *ptDev = get_serial_device("usart0"); uint8_t chString[] = "Hello World!"; //! 我们就可以访问这个对象的方法,比如发送字符串
while ( fsm_rt_cpl !=
ptDev->method.write(ptDev, chString, sizeof(chString))
);
//! 当然这个对象仍然是被掩码结构体保护的,因为ptDev的另外一个可见的成员是ptDev->chMask,你懂的

接下来我们要处理的问题就是继承和派生。。。哎,绕这么大圈子才切入本文的重点。记得有个谚语的全文叫“博士

卖驴,下笔千言,离题万里,未有驴子。。。”,要实现继承和派生只要借助下面这个装模作样的宏就可以了

 //! \brief macro for inheritance
#define INHERIT(__TYPE) __TYPE base;

是的他不过是把基类作为新类(结构体)的第一个元素,并起了一个好听的名字base。尼玛太坑爹了吧?没错其实

就是这样,没有什么复杂的,所以我们可以很容易的从serial_dev_t继承并为usart派生出一个类:

 #include "serial_device.h"
...
EXTERN_CLASS(usart_t) INHERIT(serial_dev_t) uint8_t chName[]; //!< 保存名字,比如"USART0"
usart_reg_t *ptRegisters; //!< 指向设备寄存器
... END_EXTERN_CLASS(usart_t) //! \brief 当然要提供一个函数来返回基类咯
extern serial_dev_t *usart_get_base(usart_t *ptUSART);

完成了这些,关于OOC格式上的表面工作,基本介绍完毕。格式毕竟是表面工作,学会这些并不意味让你的代码面向

对象,最多看起来很高档。真正关键的是给自己面向对象的思维模式和训练自己相应的开发方法,这就需要你去看那些

介绍面向对象方法的书了。比如面向对象的思想啊,设计模式啊,uml建模啊还是那句老话,如果过不知道怎么入门看

《UML+OOPC》

模块的封装之C语言类的继承和派生的更多相关文章

  1. 模块的封装之C语言类的封装

    [微知识]模块的封装(一):C语言类的封装 是的,你没有看错,我们要讨论的是C语言而不是C++语言中类的封装.在展开知识点之前,我首先要 重申两点: 1.面向对象是一种思想,基本与所用的语言是无关的. ...

  2. C++学习笔记:07 类的继承与派生

    课程<C++语言程序设计进阶>清华大学 郑莉老师) 基本概念 继承与派生的区别: 继承:保持已有类的特性而构造新类的过程称为继承. 派生:在已有类的基础上新增自己的特性(函数方法.数据成员 ...

  3. [C++]类的继承与派生

    继承性是面向对象程序设计的第二大特性,它允许在既有类的基础上创建新类,新类可以继承既有类的数据成员和成员函数,可以添加自己特有的数据成员和成员函数,还可以对既有类中的成员函数重新定义.利用类的继承和派 ...

  4. 09--c++ 类的继承与派生

    c++ 类的继承与派生   一.基本概念 1.类的继承,是新的类从已有类那里得到已有的特性.或从已有类产生新类的过程就是类的派生.原有的类称为基类或父类,产生的新类称为派生类或子类.   2.派生类的 ...

  5. Day 5-2 类的继承和派生,重用

    类的继承 派生 在子类中重用父类 组合 抽象类 定义: 继承指的是类与类之间的关系,是一种什么“是”什么的关系,继承的功能之一就是用来解决代码重用问题. 继承是一种创建新类的方式,在python中,新 ...

  6. Python基础(16)_面向对象程序设计(类、继承、派生、组合、接口)

    一.面向过程程序设计与面向对象程序设计 面向过程的程序设计:核心是过程,过程就解决问题的步骤,基于该思想设计程序就像是在设计一条流水线,是一种机械式的思维方式 优点:复杂的问题的简单化,流程化 缺点: ...

  7. Python3 面向对象-类的继承与派生

    1.什么是继承? 继承是一种创建新类的方式,新建的类可以继承一个或多个父类(python支持多继承),父类可称为基类或超类,新建的类称为派生类和或子类. 子类会遗传父类的属性,从而解决代码重用问题. ...

  8. 4-13 object类,继承和派生( super) ,钻石继承方法

    1,object 类 object class A: ''' 这是一个类 ''' pass a = A() print(A.__dict__) # 双下方法 魔术方法 创建一个空对象 调用init方法 ...

  9. 对C++类的继承和派生的理解

    C++中的继承是类与类之间的关系,是一个很简单很直观的概念,与现实世界中的继承类似,例如儿子继承父亲的财产. 1.继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程. ...

随机推荐

  1. 策略模式原理及Java代码实例

    一.策略模式的定义 —— 定义了一组算法,将每个算法包装起来,并且使它们之间可以互换 —— 策略模式使这些算法在客户端调用它们的时候能够相互不影响的变化,改变不同算法的实现方式不影响客户端的使用,即策 ...

  2. java高级---->Thread之FutureTask的使用

    FutureTask类是Future 的一个实现,并实现了Runnable,所以可通过Excutor(线程池) 来执行,也可传递给Thread对象执行.今天我们通过实例来学习一下FutureTask的 ...

  3. CSS-自定义高度的元素背景图如何自适应以及after伪元素在ie下的处理

    我都好久没更新了! 遇到一个效果,之前没有考虑清楚,设置了固定高度,到了后边,产品要加长,我就觉得设计得从新弄张长点的背景图!这不多余么? 其实分析原图还是可以再切分,再细化到不用改设计图,让我们前端 ...

  4. Windows 2008 server R2安装.NET Framework4时提示“灾难性故障”

    报错信息: 安装.NET Framework 4时,提示安装未成功,“灾难性故障”.服务器的操作系统是windows  server 2008 R2. 查看系统日志时显示“无法安装 Windows 更 ...

  5. spfile与pfile

    SYS@ora11g>show parameter spfile NAME TYPE------------------------------------ ------------------ ...

  6. docker stack命令

    启动docker docker stack deploy -c compose-filename.yml service-name docker stack rm service-name

  7. HDU_2888_Check Corners

    Check Corners Time Limit: 2000/10000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)To ...

  8. rac库grid目录权限(6751)导致数据库宕机案例 此方法仅用于紧急救助

    问题: 我的rac环境不小心通过chown命令改变了/u01目录及其子目录的权限,导致rac节点2数据库宕掉,sqlplus下打开数据库报错如下: [oracle@node2 ~]$ sqlplus ...

  9. CRM - 销售与客户

    一.销售与客户 - 表结构 ---公共客户(公共资源) 1.没有报名 2.3天没有跟进 3.15天没有成单 客户分布表 龙泰 男 yuan 2018-5-1 3天未跟进 龙泰 男 三江 2018-5- ...

  10. 约会安排---hdu4553(线段树,麻烦的区间覆盖)

      题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4553 算是poj3667的加强版,建立两颗线段树,一个是DS区间,另一个是NS区间.那么根据题意, ...