出处:
[微知识]模块的封装(一):C语言类的封装
是的,你没有看错,我们要讨论的就是C语言而不是C++语言中类的封装。在展开知识点之前,我首先要
重申两点:
1、面向对象是一种思想,基本与所用的语言是无关的。当你心怀面向对象,即便用QBasic也能写出符合
面向对象思想的代码,更不用说是C语言了。举一个反例来说,很多人初学C++的时候,并没有掌握
面向对象的思想,活生生把类当结构体来用的,也不在少数吧?
2、面向对象的最基本出发点是“将数据以及处理数据的方法封装在一起”。至于继承、派生、多态之类
则是后面扩展的东西。在C语言中,如果用结构体来保存数据,并将处理这些数据的函数与结构体的定
义封装在同一个.c文件中,则该.c文件就可以视作是一个类。如果将指向具体函数的函数指针与结构体
的其它成员变量封装在同一个结构体中,则该“对象”的使用甚至就与C++相差无几了。
以上内容是面向对象C语言(Object-Oriented C Programming with ANSI-C)技术的基本出发点。作为引
子,在使用OOC技术的时候,我们会遇到这么一个问题:是的,我们可以用结构体来模拟类,将所有的成员变
量都放在结构体中,并将这一结构体放置在类模块的接口头文件中(xxxx.h),但问题是,结构体里面的成员
变量都是public的,如何保护他们使其拥有private属性呢?解决的方法就是使用掩码结构体(Masked Structure)
那么,什么是掩码结构体呢?在回答这个问题之前,我们先看看下面这个例子。已知我们定义了一些用于
在C语言里面进行类封装的宏,如下所示:
1.
2.
3. #define EXTERN_CLASS(__NAME,...) \
4. typedef union __NAME __NAME;\
5. __VA_ARGS__\
6. union __NAME {\
7. uint_fast8_t chMask[(sizeof(struct {
8.
9. #define END_EXTERN_CLASS(__NAME) \
10. }) + sizeof(uint_fast8_t) - 1) /sizeof(uint_fast8_t)];\
11. };
12.
13.#define DEF_CLASS(__NAME,...)\
14. typedef union __NAME __NAME;\
15. __VA_ARGS__\
16. typedef struct __##__NAME __##__NAME;\
17. struct __##__NAME{
18.
19.#define END_DEF_CLASS(__NAME) \
20. };\
21. union __NAME {\
22. uint_fast8_tchMask[(sizeof(__##__NAME) + sizeof(uint_fast8_t) - 1) /sizeof(uint_fast8_t)];\
23. };
24.
25.#define CLASS(__NAME) __##__NAME
26.
27.
复制代码
假设我们要封装一个基于字节的队列类,不妨叫做Queue,因此我们建立了一个类文件queue.c
和对应的接口头文件queue.h。假设我们约定queue.c将不包含queue.h(这么做的好处很多,在以后的
内容里再讲解,当然对掩码结构体技术来说,模块的实现是否包含模块的接口头文件并不是关键)。
我们首先想到的是要定义一个类来表示队列,它的一个可能的形式如下
1.
2. //! \name byte queue
3. //! @{
4. typedef struct {
5. uint8_t *pchBuffer; //!< queue buffer
6. uint16_t hwBufferSize; //!< buffer size
7. uint16_t hwHead; //!< headpointer
8. uint16_t hwTail; //!< tailpointer
9. uint16_t hwCounter; //!< byte counter
10.}queue_t;
11.//! @}
复制代码
目前为止,一切都还OK。由于queue.c并不包含queue.h,因此我们决定在两个文件中各放一个定义。由于.h
中包含了队列的完整数据信息,使用该模块的人可能会因为种种原因直接访问甚至修改队列结构体中的数据
——也许在这个例子中不是那么明显,但是在你某个其它应用模块的例子中,你放在结构体里面的某个信息
可能对模块的使用者来说,直接操作更为便利,因此悲剧发生了——原本你假设“所有操作都应该由queue.c
来完成的”格局被打破了,使用者可以轻而易举的修改和访问结构体的内容——而这些内容在面向对象思想中
原本应该是私有的,无法访问的(private)。原本测试完好的系统,因为这种出乎意料的外界干涉而导致不稳
定,甚至是直接crash了。当你气冲冲的找到这么“非法”访问你结构体的人时,对方居然推了推眼镜一脸无辜
的看着你说“根据接口的最小信息公开原则,难道你放在头文件里面的信息不是大家放心可以用的么?”
OTZ...哑口无言,然后你会隐约觉得太阳穴微微的在跳动……
且慢,如果我们通过一开始提供的宏分别对queue.h和queue.c中的定义改写一番,也许就是另外一个局面了:
queue.h
1. ...
2. //! \name byte queue
3. //! @{
4. EXTERN_CLASS(queue_t)
5. uint8_t *pchBuffer; //!< queue buffer
6. uint16_t hwBufferSize; //!< buffer size
7. uint16_t hwHead; //!< headpointer
8. uint16_t hwTail; //!< tailpointer
9. uint16_t hwCounter; //!< byte counter
10.END_EXTERN_CLASS(queue_t)
11.//! @}
12....
13.extern bool queue_init(queue_t *ptQueue, uint8_t *pchBuffer, uint16_thwSize);
14.extern bool enqueue(queue_t *ptQueue, uint8_t chByte);
15.extern bool dequeue(queue_t *ptQueue, uint8_t *pchByte);
16.extern bool is_queue_empty(queue_t *ptQueue);
17....
18.
复制代码
queue.c
1. ...
2. //! \name byte queue
3. //! @{
4. DEF_CLASS(queue_t)
5. uint8_t *pchBuffer; //!< queuebuffer
6. uint16_t hwBufferSize; //!< buffer size
7. uint16_t hwHead; //!< headpointer
8. uint16_t hwTail; //!< tailpointer
9. uint16_t hwCounter; //!< byte counter
10.END_DEF_CLASS(queue_t)
11.//! @}
12....
复制代码
对照前面的宏,我们实际上可以手工将上面的内容展开(这里就不再赘述了),可以看到,实际上类型queue_t
是一个掩码结构体,里面只有一个起到掩码作用的数组chMask,其大小和真正后台的的类型__queue_t相同——
这就是掩码结构体实现私有成员保护的秘密。解决了私有成员保护的问题,剩下还有一个问题,对于queue.c的
函数来说queue_t只是一个数组,那么正常的功能要如何实现呢?下面的代码片断将为你解释一切:
1. ...
2. bool is_queue_empty(queue_t *ptQueue)
3. {
4. CLASS(queue_t) *ptQ = (CLASS(queue_t) *)ptQueue;
5. if (NULL == ptQueue) {
6. return true;
7. }
8. return ((ptQ->hwHead == ptQ->hwTail) && (0 ==ptQ->Counter));
9. }
10....
复制代码
从编译器的角度来说,这种从queue_t到__queue_t类型指针的转义是逻辑上的,并不会因此产生额外的代码,
简而言之,使用掩码结构体几乎是没有代价的——如果你找出了所谓的代价,一方面不妨告诉我,另一方面,
不妨考虑这个代价和模块的封装相比是否是可以接受的。欢迎您的讨论。
全文完
[交流][微知识]模块的封装(二):C语言类的继承和派生
在模块的封装(一):C语言类的封装中,我们介绍了如何使用C语言的结构体来实现一个类的封装,并通过掩码结构体的方式实
现了类成员的保护。这一部分,我们将在此基础上介绍C语言类的继承和派生。其实继承和派生是同一个动作的两种不同角度的表述。
当我们继承了一个基类而创造了一个新类时,派生的概念就诞生了。派生当然是从基类派生的。派生出来的类当然继承了基类的东西。
继承和派生不是一对好基友,他们根本就是一个动作的两种不同说法,强调动作的起始点的时候,我们说这是从某某类继承来的;强调
动作的终点时,我们说派生出了某某类。——呼……真累……厄……不是阿……我不是唐僧。
我们知道,类总是会提供一些方法,可以让我们很方便的使用,比如
1.
2. window_t tWin = new_window(); //!<创建一个新的window对象
3. tWin.show(); //!<显示窗体
4.
复制代码
显然,能够实现这一技术的必要手段就是将函数指针一起封装在结构体中。在C语言中,类的方法(method)是通过函数指针(或者函
数指针的集合)——我们叫做虚函数(表)来实现的。虚函数表同样可以单独存在,我们称之为interface。在C语言中,虚函书表是可以
直接通过封装了纯函数指针的结构体来实现的。如下面的代码所示:
1. //! \name interfacedefinition
2. //! @{
3. #defineDEF_INTERFACE(__NAME,...) \
4. typedef struct __NAME __NAME;\
5. __VA_ARGS__\
6. struct __NAME {
7.
8. #defineEND_DEF_INTERFACE(__NAME) \
9. };
10. //! @}
11.
复制代码
例如,我们可以使用上面的宏来定义一个字节流的读写接口:
1.
2. DEF_INTERFACE(i_pipe_byte_t)
3. bool(*write)(uint8_t chByte);
4. bool(*read)(uint8_t *pchByte)
5. END_DEF_INTERFACE(i_pipe_byte_t)
6.
复制代码
这类接口非常适合定义一个模块的依赖型接口——比如,某一个数据帧解码的模块是依赖于对字节流的读写的,通过在该模块中使用这样一个接口,
并通过专门的接口注册函数,即可实现所谓的面向接口开发——将模块的逻辑实现与具体应用相关的数据流隔离开来。例如:
frame.c
1.
2. ...
3. DEF_CLASS(frame_t)
4. i_pipe_byte_t tStream; //!<流接口
5. ...
6. END_DEF_CLASS(frame_t)
7.
8. //!接口注册函数
9. boolframe_register_stream_interface(frame_t *ptFrame, i_pipe_byte_t tStream)
10. {
11. //!去除掩码结构体的保护
12. CLASS(frame_t) *ptF = (CLASS(frame_t) *)ptFrame;
13. //!合法性检查
14. if(NULL == tStream.write || NULL == tStream.read || NULL == ptFrame ) {
15. return false;
16. }
17. ptF->tStream = tStream; //!<设置接口
18. returntrue;
19. }
20.
复制代码
frame.h
1.
2. ...
3. EXTERN_CLASS(frame_t)
4. i_pipe_byte_t tStream; //!<流接口
5. ...
6. END_EXTERN_CLASS(frame_t)
7.
8. //!接口注册函数
9. extern boolframe_register_stream_interface(frame_t *ptFrame, i_pipe_byte_t tStream);
10.
11. extern boolframe_init(frame_t *ptFrame);
12.
13.
复制代码
基于这样的模块,一个可能的外部使用方法是这样的:
app.c
1.
2. ...
3. static boolserial_out(uint8_t chByte)
4. {
5. ...
6. }
7.
8. static boolserial_in(uint8_t *pchByte)
9. {
10. ...
11. }
12.
13. static frame_ts_tFrame;
14. ...
15. void app_init(void)
16. {
17. //!初始化
18. frame_init(&s_tFrame);
19.
20. //!初始化接口
21. do {
22. i_pipe_byte_t tPipe = {&serial_out, &serial_in};
23. frame_register_stream_interface(&s_tFrame, tPipe);
24. }while(0);
25. }
26.
复制代码
像这个例子展示的这样,将接口直接封装在掩码结构体中的形式,我们并不能将其称为“实现(implement)了接口i_pipe_byte_t”,
这只是内部将虚函数(表)表作为了一个普通的成员而已,我们可以认为这是加入了private属性的,可重载的内部成员函数。下面,我们
将来介绍如何真正的“实现(implement)”指定的接口。首先,我们要借助下面的专门定义的宏:
1.
2. #defineDEF_CLASS_IMPLEMENT(__NAME,__INTERFACE,...)\
3. typedefunion __NAME __NAME;\
4. __VA_ARGS__\
5. typedefstruct __##__NAME __##__NAME;\
6. struct__##__NAME {\
7. const__INTERFACE method;
8.
9. #defineEND_DEF_CLASS_IMPLEMENT(__NAME,__INTERFACE)\
10. };\
11. union__NAME {\
12. const __INTERFACE method;\
13. uint_fast8_t chMask[(sizeof(__##__NAME) +sizeof(uint_fast8_t) - 1) / sizeof(uint_fast8_t)];\
14. };
15.
16. #defineEXTERN_CLASS_IMPLEMENT(__NAME,__INTERFACE,...) \
17. typedefunion __NAME __NAME;\
18. __VA_ARGS__\
19. union__NAME {\
20. const __INTERFACE method;\
21. uint_fast8_t chMask[(sizeof(struct {\
22. const __INTERFACE method;
23.
24. #define END_EXTERN_CLASS_IMPLEMENT(__NAME,__INTERFACE) \
25. }) + sizeof(uint_fast8_t) - 1) / sizeof(uint_fast8_t)];\
26. };
27.
28.
复制代码
为了很好的说明上面宏的用法,我们以一个比较具体的例子来示范一下。这是一个通用的串行设备驱动的例子,这个例子的
意图是,为所有的类似USART,I2C,SPI这样的串行数据接口建立一个基类,随后,不同的外设都从该基类继承并派生出
属于自己的基类,比如USART类等等——这种方法是面向对象开发尤其是面向接口开发中非常典型的例子。首先,我们要
定义一个高度抽象的接口,该接口描述了我们是期待如何最简单的使用一个串行设备的,同时一起定义实现了该类的基类
serial_dev_t:
serial_device.h
1.
2. //!这是一个实现了接口i_serial_t的基类serial_dev_t
3. EXTERN_CLASS_IMPLEMENT(serial_dev_t, i_serial_t,
4.
5. //!这是我们定义的接口i_serial_t这里的语法看起来似乎有点怪异,后面将介绍
6. DEF_INTERFACE( i_serial_t)
7. fsm_rt_t (*write)(serial_dev_t *ptDev, uint8_t*pchStream, uint_fast16_t hwSize); //!< i_serial_t接口的write方法
8. fsm_rt_t (*read)(serial_dev_t *ptDev, uint8_t*pchStream, uint_fast16_t hwSize); //!< i_serial_t接口的read方法
9. END_DEF_INTERFACE( i_serial_t )
10. )
11. //!类serial_dev_t的内部定义
12. ...
13. END_EXTERN_CLASS_IMPLEMENT(serial_dev_t, i_serial_t )
14.
复制代码
如果不仔细看,这个例子似乎比较清楚了,一个基类serial_dev_t实现了接口i_serial_t。但仔细一看这里不光语法奇怪,
而且还有很多细节。
首先,接口居然是定义在类的定义里面的,而且是定义在参数宏EXTERN_CLASS_IMPLEMENT里面的!
其次,似乎类serial_dev_t在接口i_serial_t定义之前就已经能implement它了,而且接口i_serial_t也反过来在自己的定义
中引用了基类serial_dev_t。如果你曾经定义过类似下面的结构体,你就知道蹊跷在哪里了,同时,你也就知道解决的
原理了:
1.
2. //!一个无法编译通过的写法
3. typedef struct {
4. ....
5. item_t*ptNext;
6. }item_t;
7.
复制代码
等效的正确写法如下:
1.
2. //!前置声明的例子
3. typedef struct item_titem_t;
4. struct item_t {
5. ...
6. item_t*ptNext;
7. };
8.
复制代码
可见,前置声明是解决这类问题的关键,回头再看看EXTERN_CLASS_IMPLEMENT的宏,你就会看到前置声明的结构。
以此为例,我来演示一下如何用参数宏实现方便的前置声明:
1.
2. #define DEF_FORWARD_LIST(__NAME) \
3. typedefstruct __NAME __NAME;\
4. struct__NAME {
5.
6. #defineEND_DEF_FORWARD_LIST(__NAME) \
7. };
8.
复制代码
使用的时候这样
1.
2. DEF_FORWARD_LIST(item_t)
3. ...
4. item_t*ptNext;
5. END_DEF_FORWARD_LIST(item_t)
6.
复制代码
这只解决了一个疑惑,另外一个疑惑就是为什么可以在参数宏里面插入另外一段代码?答案是一直可以,比如,我常这么干:
1.
2. # defineSAFE_ATOM_CODE(...) {\
3. istate_t tState = GET_GLOBAL_INTERRUPT_STATE();\
4. DISABLE_GLOBAL_INTERRUPT();\
5. __VA_ARGS__;\
6. SET_GLOBAL_INTERRUPT_STATE(tState);\
7. }
8.
复制代码
这是原子操作的宏,使用的时候,只要在"..."的位置写程序就好了,例如:
adc.c
1.
2. ...
3. static volatileuint16_t s_hwADCResult;
4. ...
5. ISR(ADC_vect)
6. {
7. //!获取ADC的值
8. s_hwADCResult = ADC0;
9. }
10.
11. //! \brief带原子保护的adc结果读取
12. uint16_tget_adc_result(void)
13. {
14. uint16_t hwResult;
15. SAFE_ATOM_CODE(
16. hwResult = s_hwResult;
17. )
18. returnhwResult;
19. }
20.
复制代码
adc.h
1.
2. ...
3. //!可以随时安全的读取ADC的结果
4. extern uint16_tget_adc_result(void);
5. ...
6.
复制代码
现在看来在参数宏里面插入大段大段的代码根本不是问题,问题是,当我不想插入的时候怎么办呢?例如这个例子里面,宏
EXTERN_CLASS_IMPLEMENT(__NAME, __INTERFACE,...)这里,我们真正关心的是__NAME和__INTERFACE,而是否插入
其它代码到定义结构里面是不确定的,我们很可能就直接想这么用
1.
2. EXTERN_CLASS_IMPLEMENT(example_t,i_serial_t)
3. ....
4. END_EXTERN_CLASS_IMPLEMENT(example_t,i_serial_t)
5.
复制代码
显然,这时候变长参数就成了关键,幸好C99位我们提供了这个便利,直接在参数宏里面加入“...”在宏本体里面用
__VA_ARGS__就可以代表“...”的内容了。
经过这样的解释,回头再去看前面的类定义,根本不算什么。^_^
那么一个类实现(implement)了某个接口,这有神马意义呢?意义如下,我们就可以像正常类那么使用接口提供的方法了:
1.
2. //!假设我们获取了一个名叫“usart0”的串行设备
3. serial_dev_t *ptDev =get_serial_device("usart0");
4.
5. uint8_t chString[] ="Hello World!";
6.
7. //!我们就可以访问这个对象的方法,比如发送字符串
8. while ( fsm_rt_cpl!=
9. ptDev->method.write(ptDev, chString, sizeof(chString))
10. );
11. //!当然这个对象仍然是被掩码结构体保护的,因为ptDev的另外一个可见的成员是ptDev->chMask,你懂的
12.
复制代码
接下来,我们要处理的问题就是继承和派生……唉,绕了这么大的圈子,才切入本文的重点。记得有个谚语的全文叫做“博士卖驴,
下笔千言,离题万里,未有驴子……”
要实现继承和派生,只要借助下面这个装模作样的宏就可以了。
1.
2. //! \brief macro forinheritance
3. #defineINHERIT(__TYPE) __TYPEbase;
4.
复制代码
是的,它不过是把基类作为新类(结构体)的第一个元素,并起了一个好听的名字叫base。“尼玛坑爹了吧?”没错,其实就是这
样,没什么复杂的,所以我们可以很容易的从serial_dev_t继承并为usart派生一个类出来:
一个绘图无关的窗体系统的例子
window.h
1.
2.
3. //! \name矩形区域
4. //! @{
5. typedef struct {
6. int32_t nTop;
7. int32_t nLeft;
8. int32_t nHeigh;
9. int32_t nWidth;
10. } rect_t;
11. //! @}
12.
13.
14.
15. //! \brief定义一个window类,它是一切的起点,也是一个抽象类abstract class,实现了接口i_win_t
16. EXTERN_CLASS_IMPLEMENT(window_t,i_win_t,
17. //! \name基于接口开发的window当然要有对应的接口咯,这也是未来覆盖(override)的关键
18. DEF_INTERFACE(i_win_t,
19. fsm_rt_t win_method_t(window_t *ptWin); //!< window类里面的会用到的虚函数原型
20. fsm_rt_t win_msg_process(window_t *ptWin, msg_t tMSG); //!<消息处理函数
21. )
22. win_method_t *fnRefresh; //!<一个虚函数,用来刷新窗体的,你懂的
23. win_msg_process *fnMSGProcess; //!<一个虚函数窗体怎么能没有消息处理函数呢,这可是事件驱动的关键阿
24. END_DEF_INTERFACE
25. )
26. rect_t tRegion; //!< window的位置和大小
27. bool bIsPressed; //!<是否是被按下的效果
28. END_EXTERN_CLASS_IMPLEMENT(window_t,i_win_t)
29.
30.
复制代码
control.h
1.
2.
3. //! \name一个抽象类,它继承了基类window_t
4. EXTERN_CLASS(control_t)INHERIT(window_t)
5.
6. bool bIsTableStop; //!< Table键是否能停在这个控件上
7. bool bIsActivated; //!<当前控件是否处于激活状态
8. i_control_t *ptPrevious;
9. i_control_t *ptNext;
10.
11. END_EXTERN_CLASS(control_t)
12.
13.
复制代码
[交流][微知识]模块的封装(三):无伤大雅的形式主义
在前面的讨论中,我们介绍了如何在必要的情况下,利用结构体和联合体在C语言中引入应用所必须的一些面向对象的
特性,例如,封装、继承、派生以及覆盖。从功能性上来说,前两章的内容就足够满足应用了;但从形式上来说,模块封装
(二)中提供的类的继承方式在使用的时候还有那么一点不爽,所以,作为弥补一小部分有代码洁癖人的心头缺憾,今天我
们就来讨论一种在不增加任何实质性系统开销的前提下实现对象成员访问的形式上的改进。
特别提示:本帖仅用于娱乐,C++党请本着娱乐的心情吐槽和嘲讽,避免引起C党在代码效率上的反驳。形式主义
就是形式主义,没有用,就是自己看着舒服
首先我们来说说哪里不爽了,举个例子,我们定义了一个接口,用于封装一些针对存储器页(Page)的操作:
1.
2. //! \name memory pageaccess interface
3. //! @{
4. DEF_INTERFACE(i_mem_page_t)
5.
6. fsm_rt_t (*PageWrite) (mem_t *ptMal, uint32_t wPageAddress, void*ptBuffer);
7. fsm_rt_t (*PageRead) (mem_t *ptMal, uint32_t wPageAddress, void*ptBuffer);
8. fsm_rt_t (*PageErase) (mem_t *ptMal, uint32_t wPageAddress);
9. fsm_rt_t (*Erase) (mem_t *ptMal);
10.
11. END_DEF_INTERFACE(i_mem_page_t)
12. //! @}
13.
复制代码
这个接口本身没什么太特别的,就是定义了针对所有以Page的基本单位的存储器常见的四个操作:页写,页读,页擦除和
整片擦除。接口类型叫做i_mem_page_t,顾名思义,就是一个针对memory page的接口,详细内容不提也罢。接下来我
们又定义了另外一个接口i_mem_t,这个接口试试图为存储器抽象一个更通用的操作方式,比如Init, Open, Close之类的,
为了方便日后的统一操作,这里不妨也定义成接口i_mcb_t(这里mcb是memory control block的缩写),新的接口i_mem_t
就同时继承了i_mem_page_t和i_mcb_t,如下所示:
1.
2. //! \name memorycontrol block
3. //! @{
4. DEF_INTERFACE(i_mcb_t)
5.
6. fsm_rt_t (*Init) (mem_t *ptMal, void *ptCFG);
7. fsm_rt_t (*Finish) (mem_t *ptMal);
8. mem_info_t (*Info) (mem_t *ptMal);
9. fsm_rt_t (*Open) (mem_t *ptMal);
10. fsm_rt_t (*Close) (mem_t *ptMal);
11. mem_status_t (*GetStatus) (mem_t *ptMal);
12.
13. END_DEF_INTERFACE(i_mcb_t)
14. //! @}
15.
16. //! \name MemoryAbstraction Layers
17. //! @{
18. DEF_INTERFACE(i_mem_t)
19.
20. i_mcb_tCONTRL;
21. i_mem_page_t PAGE;
22. ...
23.
24. END_DEF_INTERFACE(i_mem_t)
25. //! @}
26.
复制代码
好的,问题来了:对一小部分人来说,下面的代码是有点不可容忍的,套用一句话说就是“好焦虑”。好焦虑对吧?
不要怀疑,为了凸显这种莫名的焦虑,我特别用了红色。
1.
2. i_mem_t *ptMEM =xxxxxx;
3. ...
4. ptMEM->CONTRL.Open(); //!打开存储器
5.
复制代码
这里所谓的多按一次“.”是针对那些有联想提示功能的IDE说的,因为通过上述方法,所谓的实现接口(Implement)实际
上是通过简单的添加接口的成员变量来实现的,在这种情况下访问继承接口的成员函数必须要先通过这个成员变量,比如例子
中的CONTRL。对某些人来说上面的例子代码应该支持这种形式才好:
1.
2. i_mem_t *ptMEM =xxxxxx;
3. ...
4. ptMEM->Open(); //!<直接打开存储器
5. ...
6. i_mcb_t *ptMCB =&(ptMEM->CONTRL); //!< CONTRL仍然存在
7.
复制代码
简单来说就是既可以直接访问被继承的接口的成员,又可以保留对接口的直接引用。做到第一点并不难,只要通过下面的
代码就可以了:
1.
2. //! \name MemoryAbstraction Layers
3. //! @{
4. DEF_INTERFACE(i_mem_t)
5.
6. fsm_rt_t (*Init) (mem_t *ptMal, void *ptCFG);
7. fsm_rt_t (*Finish) (mem_t *ptMal);
8. mem_info_t (*Info) (mem_t *ptMal);
9. fsm_rt_t (*Open) (mem_t *ptMal);
10. fsm_rt_t (*Close) (mem_t *ptMal);
11. mem_status_t (*GetStatus) (mem_t *ptMal);
12.
13. fsm_rt_t (*PageWrite) (mem_t *ptMal, uint32_t wPageAddress, void*ptBuffer);
14. fsm_rt_t (*PageRead) (mem_t *ptMal, uint32_t wPageAddress, void*ptBuffer);
15. fsm_rt_t (*PageErase) (mem_t *ptMal, uint32_t wPageAddress);
16. fsm_rt_t (*Erase) (mem_t *ptMal);
17.
18. END_DEF_INTERFACE(i_mem_t)
19.
复制代码
看到这里,一群人就要“呵呵”了,我也“呵呵”,你会管这个叫做对接口i_mcb_t和i_mem_page_t的继承么?你骗谁呢!
a . 如果修改了i_mcb_t或者i_mem_page_t的内容,我们还需要一起修改所有“所谓继承”了他们的接口,这哪里是
面向接口开发?这简直就是面向麻烦啊,我都不会同意
b. 有些应用就是纯粹的面向接口(虚函数表)的,因此必须要保留对原有接口的引用能力。因此CONTRL和PAGE是
必须要保留的。
要想同时保留名副其实的“继承”,又想有能力直接访问被继承接口的成员,就只有借助ANSI-C11标准引入的匿名结构
体了。上述例子的解决方案如下:
1.
2. //! \name MemoryAbstraction Layers
3. //! @{
4. DEF_INTERFACE(i_mem_t)
5.
6. union{
7. struct {
8. fsm_rt_t (*Init) (mem_t *ptMal,void *ptCFG);
9. fsm_rt_t (*Finish) (mem_t *ptMal);
10. mem_info_t (*Info) (mem_t *ptMal);
11. fsm_rt_t (*Open) (mem_t *ptMal);
12. fsm_rt_t (*Close) (mem_t*ptMal);
13. mem_status_t (*GetStatus) (mem_t *ptMal);
14. };
15. i_mcb_t CONTRL;
16. };
17. union{
18. struct {
19. fsm_rt_t (*PageWrite) (mem_t*ptMal, uint32_t wPageAddress, void *ptBuffer);
20. fsm_rt_t (*PageRead) (mem_t*ptMal, uint32_t wPageAddress, void *ptBuffer);
21. fsm_rt_t (*PageErase) (mem_t*ptMal, uint32_t wPageAddress);
22. fsm_rt_t (*Erase) (mem_t *ptMal);
23. };
24. i_mem_page_t PAGE;
25. };
26.
27. END_DEF_INTERFACE(i_mem_t)
28. //! @}
29.
复制代码
其实匿名一直是很强大的——比如匿名结构体啊,匿名联合体啊,比如,其实上面的方式可以写成如下的形式(感谢15楼剧透):
1.
2. //! \name MemoryAbstraction Layers
3. //! @{
4. DEF_INTERFACE(i_mem_t)
5.
6. union{
7. i_mcb_t;
8. i_mcb_t CONTRL;
9. };
10. union{
11. i_mem_page_t;
12. i_mem_page_t PAGE;
13. };
14.
15. END_DEF_INTERFACE(i_mem_t)
16. //! @}
17.
复制代码
如果你不想保留对原有接口的引用,你甚至可以这么写:
1.
2. //! \name MemoryAbstraction Layers
3. //! @{
4. DEF_INTERFACE(i_mem_t)
5.
6. i_mcb_t;
7. i_mem_page_t;
8.
9. END_DEF_INTERFACE(i_mem_t)
10. //! @}
11.
复制代码
肿么样,强大的cry了吧。接下来,让我们把形式主义进行到底,首先来定义一个宏:
1.
2. //! \brief macro forinheritance
3.
4. #defineINHERIT_EX(__TYPE, __NAME) \
5. union { \
6. __TYPE __NAME; \
7. __TYPE; \
8. };
9.
10. /*! \note Whenderiving a new class from a base class, you should use INHERIT
11. * other than IMPLEMENT, although they looks the same now.
12. */
13. #defineINHERIT(__TYPE) INHERIT_EX(__TYPE, base__##__TYPE)
14.
15. /*! \note You canonly use IMPLEMENT when defining INTERFACE. For Implement
16. * interface when defining CLASS, you should useDEF_CLASS_IMPLEMENT
17. * instead.
18. */
19. #defineIMPLEMENT(__INTERFACE) INHERIT_EX(__INTERFACE,base__##__INTERFACE)
20.
21. /*! \note if you haveused INHERIT or IMPLEMENT to define a CLASS / INTERFACE,
22. you can use OBJ_CONVERT_AS to extract the reference to theinherited
23. object.
24. \*/
25. #defineOBJ_CONVERT_AS(__OBJ, __INTERFACE) (__OBJ.base__##__INTERFACE)
26.
复制代码
然后自然就得到以下的代码:
1.
2. //! \name MemoryAbstraction Layers
3. //! @{
4. DEF_INTERFACE(i_mem_t) INHERIT(i_mcb_t) INHERIT(i_mem_page_t)
5. ...
6. ... //!继承后增加的成员
7. ...
8. END_DEF_INTERFACE(i_mem_t)
9. //! @}
10.
复制代码
对于i_mem_t的对象,如果我们想访问其继承的接口,可以通过下面的代码来实现:
1.
2. i_mem_t*ptMEM = .....;
3. i_mcb_t*ptMCB = &OBJ_CONVERT_AS((*ptMEM), i_mcb_t);
4. ptMCB->Init(...)
5. ...
6.
复制代码
当然,普通情况下,我们只要直接用ptMEM访问Init()方法就好了。上面的方法的方法是为面向接口开发所余留的能力。
最后我们来谈一个C语言对“匿名”的支持问题。因为这是一个比较新的特性,很多C编译器并不默认支持。别的环境我们
就不管了,从事嵌入式开发,常见的几个C编译环境比如IAR,ARM_MDK,GCC等等都是支持的,但需要编译开关打开。
粘贴下面的代码到你的系统级头文件中,就可以让你的匿名代码在以上几个环境下都获得支持:
1.
2. /*------------------- Start of section using anonymous unions ------------------*/
3. #if defined(__CC_ARM)
4. //#pragmapush
5. #pragmaanon_unions
6. #elifdefined(__ICCARM__)
7. #pragmalanguage=extended
8. #elifdefined(__GNUC__)
9. /*anonymous unions are enabled by default */
10. #elifdefined(__TMS470__)
11. /* anonymous unionsare enabled by default */
12. #elifdefined(__TASKING__)
13. #pragmawarning 586
14. #else
15. #warningNot supported compiler type
16. #endif
17.
复制代码