[交流][微知识]模块的封装(二):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. JS-利用ajax获取json数据,并传入页面生成动态tab

    封装好的:ajax.js function ajax(url, fnSucc,fnFaild){ //1[创建] if(window.XMLHttpRequest){ var oAjax = new ...

  2. Java多线程详解(三)

    1)死锁 两个线程相互等待对方释放同步监视器时会出现死锁的现象,这时所有的线程都处于阻塞状态,程序无法继续向下执行. 如下就是会出现死锁的程序. 首先flag = 1,线程d1开始执行,锁住对象o1, ...

  3. Codeforces 603E Pastoral Oddities

    传送门:http://codeforces.com/problemset/problem/603/E [题目大意] 给出$n$个点,$m$个操作,每个操作加入一条$(u, v)$长度为$l$的边. 对 ...

  4. 【BZOJ4545】DQS的trie 后缀自动机+LCT

    [BZOJ4545]DQS的trie Description DQS的自家阳台上种着一棵颗粒饱满.颜色纯正的trie. DQS的trie非常的奇特,它初始有n0个节点,n0-1条边,每条边上有一个字符 ...

  5. Redis对于key的操作命令

     del key1 key2 ... Keyn 作用: 删除1个或多个键 返回值: 不存在的key忽略掉,返回真正删除的key的数量 rename key newkey 作用: 给key赋一个新的ke ...

  6. 【NotePad++】使用指南

    身为一名程序员,这绝对是很常用的工具,但是你真的用了他的全部功能么? 教程参考: [crifan 推荐]轻量级文本编辑器,Notepad 最佳替代品:Notepad++ 注:一个很详细的教程,虽然老, ...

  7. JS原生ajax

    原文链接:http://caibaojian.com/ajax-jsonp.html 一.JS原生ajax ajax:一种请求数据的方式,不需要刷新整个页面: ajax的技术核心是 XMLHttpRe ...

  8. vue.js(三)

    这里该记到vue的组件了,组件基础篇 1.vue组件的基本书写方式 Vue.component('button-counter', { data: function () { return { cou ...

  9. Design Pattern in Simple Examples

    Instead of defining what is design pattern lets define what we mean by design and what we mean by pa ...

  10. Flask简介之简单应用

    Flask 0.Flask简介 Flask是一个基于Python开发并且依赖jinja2模板和Werkzeug WSGI服务的一个微型框架,对于Werkzeug本质是Socket服务端,其用于接收ht ...