搜索
bottom↓
回复: 211

[2017-5-20 更新][交流][微知识]模块的封装(一):C语言类.....

  [复制链接]

出0入296汤圆

发表于 2012-11-7 22:19:41 | 显示全部楼层 |阅读模式
本帖最后由 Gorgon_Meducer 于 2019-9-5 22:46 编辑


[微知识]模块的封装(一):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. #define __EXTERN_CLASS_OBJ( __TYPE, __OBJ )         \
  2.             extern union {                          \
  3.                 CLASS(__TYPE) __##__OBJ;            \
  4.                 __TYPE   __OBJ;                     \
  5.             };
  6. #define EXTERN_CLASS_OBJ(__TYPE, __OBJ)             \
  7.             __EXTERN_CLASS_OBJ( __TYPE, __OBJ )


  8. #define __EXTERN_CLASS(__NAME,...)                  \
  9.     /*typedef union __NAME __NAME; */               \
  10.     union __NAME {                                  \
  11.         __VA_ARGS__                                 \
  12.         uint_fast8_t __NAME##__chMask[(sizeof(struct{\
  13.         __VA_ARGS__
  14. #define EXTERN_CLASS(__NAME, ...)   __EXTERN_CLASS(__NAME, __VA_ARGS__)

  15. #define END_EXTERN_CLASS(__NAME, ...)               \
  16.         }) + sizeof(uint_fast8_t) - 1) / sizeof(uint_fast8_t)];\
  17.     };

  18. #define DECLARE_CLASS(__NAME)                   \
  19.      typedef union __NAME __NAME;               

  20. #define __DEF_CLASS(__NAME,...)                 \
  21.     /*typedef union __NAME __NAME;  */          \
  22.     typedef struct __##__NAME __##__NAME;       \
  23.     struct __##__NAME {                         \
  24.         __VA_ARGS__
  25. #define DEF_CLASS(__NAME, ...)      __DEF_CLASS(__NAME, __VA_ARGS__)

  26.          
  27. #define __END_DEF_CLASS(__NAME, ...)            \
  28.     };                                          \
  29.     union __NAME {                              \
  30.         __VA_ARGS__                             \
  31.         uint_fast8_t __NAME##__chMask[(sizeof(__##__NAME) + sizeof(uint_fast8_t) - 1) / sizeof(uint_fast8_t)];\
  32.     };
  33. #define END_DEF_CLASS(__NAME, ...)  __END_DEF_CLASS(__NAME, __VA_ARGS__)

  34. #define __CLASS(__NAME)             __##__NAME
  35. #define CLASS(__NAME)               __CLASS(__NAME)

复制代码
假设我们要封装一个基于字节的队列类,不妨叫做Queue,因此我们建立了一个类文件queue.c
和对应的接口头文件queue.h。假设我们约定queue.c将不包含queue.h(这么做的好处很多,在以后的
内容里再讲解,当然对掩码结构体技术来说,模块的实现是否包含模块的接口头文件并不是关键)。

我们首先想到的是要定义一个类来表示队列,它的一个可能的形式如下

  1. //! \name byte queue
  2. //! @{
  3. typedef struct {
  4.     uint8_t *pchBuffer;    //!< queue buffer   
  5.     uint16_t hwBufferSize; //!< buffer size
  6.     uint16_t hwHead;       //!< head pointer
  7.     uint16_t hwTail;       //!< tail pointer
  8.     uint16_t hwCounter;    //!< byte counter
  9. }queue_t;
  10. //! @}
复制代码
目前为止,一切都还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;       //!< head pointer
  8.     uint16_t hwTail;       //!< tail pointer
  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_t hwSize);
  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. ...
复制代码
queue.c
  1. ...
  2. //! \name byte queue
  3. //! @{
  4. DEF_CLASS(queue_t)
  5.     uint8_t *pchBuffer;    //!< queue buffer  
  6.     uint16_t hwBufferSize; //!< buffer size  
  7.     uint16_t hwHead;       //!< head pointer
  8.     uint16_t hwTail;       //!< tail pointer
  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类型指针的转义是逻辑上的,并不会因此产生额外的代码,
简而言之,使用掩码结构体几乎是没有代价的——如果你找出了所谓的代价,一方面不妨告诉我,另一方面,
不妨考虑这个代价和模块的封装相比是否是可以接受的。欢迎您的讨论。
全文完



相关下载

    请关注 Github 来获得最新的头文件



2019-9-05 Update

    使用掩码结构体的开源OOC模板已经演进到第四版,并在github上开源,欢迎大家拍砖:
    PLOOC在github上的镜像
    完整例子:
    使用PLOOC实现队列的例子



2017-5-30 Update

    更新了掩码结构体的宏,增强了对嵌套宏的支持。根据最新的宏,更新理论讲解。提供了oopc.h头文件的下载。



2013-6-25 Update

    更新了掩码结构体的宏,使其能够支持在类中使用指向自己的指针,同时也能插入 delegate 类型
(事件处理函数原型)的声明。统一了格式。



2013-5-10 Update

    更新宏定义,解决了不同平台下CPU访问掩码结构体时可能存在的数组未对其到内核宽度的问题。
所谓对齐到内核宽度是指,当用掩码结构体的类型声明静态变量时,8位机要对齐到Byte,16位机要
对齐到Half-Word,32位机要对齐到Word。
   该更新可以直接解决MSP430在IAR环境下无法正常使用掩码结构体的问题。关于uint_fast8_t的
说明,请参照ANSI-C99关于stdint.h的描述

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

阿莫论坛20周年了!感谢大家的支持与爱护!!

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!

出0入0汤圆

发表于 2012-11-7 22:22:54 | 显示全部楼层
沙了个发

出0入0汤圆

发表于 2012-11-7 22:36:45 | 显示全部楼层
前排就坐

招租

瓜子饮料

出0入0汤圆

发表于 2012-11-7 22:49:39 | 显示全部楼层
受教了!

出110入0汤圆

发表于 2012-11-7 22:51:34 | 显示全部楼层
前排         

出0入0汤圆

发表于 2012-11-7 23:00:49 | 显示全部楼层
     前排-1

出0入0汤圆

发表于 2012-11-7 23:52:18 | 显示全部楼层
学习了!

出0入0汤圆

发表于 2012-11-8 00:03:44 | 显示全部楼层
火前留名

出0入0汤圆

发表于 2012-11-8 00:27:11 来自手机 | 显示全部楼层
如果有个例子就好了

出0入0汤圆

发表于 2012-11-8 00:37:24 | 显示全部楼层
必须mark

出0入0汤圆

发表于 2012-11-8 00:47:46 | 显示全部楼层
赶在翻页前占个座儿。

出0入0汤圆

发表于 2012-11-8 00:54:45 来自手机 | 显示全部楼层
赶紧占座

出65入0汤圆

发表于 2012-11-8 01:52:37 来自手机 | 显示全部楼层
赶紧赶紧赶紧排排坐

出0入0汤圆

发表于 2012-11-8 06:12:22 | 显示全部楼层
学习了,

出0入0汤圆

发表于 2012-11-8 07:00:38 | 显示全部楼层
楼主高人,向你学习,谢谢

出0入0汤圆

发表于 2012-11-8 07:19:09 | 显示全部楼层
Mark!!!!!!!

出0入0汤圆

发表于 2012-11-8 07:36:18 | 显示全部楼层
学习,学习!!!

出0入296汤圆

 楼主| 发表于 2012-11-8 10:19:11 | 显示全部楼层
ddqq 发表于 2012-11-8 00:27
如果有个例子就好了

例子不是给出了么?

出0入0汤圆

发表于 2012-11-8 10:35:59 | 显示全部楼层
新鲜,结构的应用,宏的应用,如此销魂。。。。佩服。。

大概就是使用宏和结构体,把结构中的变量定义隐藏得更深一些,是这个意思吗?

再看看。。。

出0入0汤圆

发表于 2012-11-8 10:53:09 | 显示全部楼层
楼底帮顶

出0入0汤圆

发表于 2012-11-8 11:02:31 | 显示全部楼层
一直很关注在C里面用OOP的思路,之前做的东西按照面向过程的思路还能凑合,一旦需要复杂的功能后,真的做的很痛苦了,所以把用C#的OOP的思路搞到了单片机里面,很多问题可以说是迎刃而解,不过前提是RAM要足够。

继续关注LZ的帖子,谢谢分享

出0入0汤圆

发表于 2012-11-8 11:20:36 | 显示全部楼层
cool                                                      

出0入0汤圆

发表于 2012-11-8 11:30:35 | 显示全部楼层
占位学习

出0入0汤圆

发表于 2012-11-8 16:00:59 来自手机 | 显示全部楼层
占位置,学习!

出0入8汤圆

发表于 2012-11-8 16:36:20 | 显示全部楼层
真心受教,继续关注········

出0入0汤圆

发表于 2012-11-8 18:09:55 | 显示全部楼层
学习了!这个方法感觉很新鲜,可以学着用用

出0入0汤圆

发表于 2012-11-8 22:59:19 | 显示全部楼层
需要认真拜读啊,谢谢

出0入0汤圆

发表于 2012-11-9 14:51:31 | 显示全部楼层
在头文件声明
typedef struct queue_t;

在c文件中定义queue_t,这样不就行了吗,这样如果试图访问结构体内容,编译都过不了。
我感觉这样的方式更好一些。

出0入0汤圆

发表于 2012-11-9 15:58:34 | 显示全部楼层
jameszxj 发表于 2012-11-9 14:51

在头文件声明
typedef struct queue_t;

在c文件中定义queue_t,这样不就行了吗,这样如果试图访问结构体内容,编译都过不了。
我感觉这样的方式更好一些。


不知道你是想说个什么意思,但是按照你的字面意思来猜测,你的想法是有问题的:C语言中声明一个结构体类型的时候,同时也是需要声明这个结构体类型的细节的。

出0入0汤圆

发表于 2012-11-9 16:07:34 | 显示全部楼层
强人!!!

出0入0汤圆

发表于 2012-11-9 16:32:12 | 显示全部楼层
本帖最后由 jameszxj 于 2012-11-9 16:43 编辑
eduhf_123 发表于 2012-11-9 15:58
不知道你是想说个什么意思,但是按照你的字面意思来猜测,你的想法是有问题的:C语言中声明一个结构体类 ...


首先声明和定义是有区别的,至于我的想法,我是实践过的,可能表述的不太明确,要用文字来表述明白还是蛮麻烦的,上个代码说明吧。
我觉得这种方法基本屏蔽了结构体的内容,麻烦的是要提供创建函数,因为调用者无法知道结构体大小。
queue.h

  1. #ifndef __QUEUE_H__
  2. #define __QUEUE_H__

  3. struct _queue_t;

  4. typedef struct _queue_t queue_t;

  5. queue_t *create_queue();

  6. void destroty_queue(queue_t *q);

  7. #endif /* __QUEUE_H__ */
复制代码
queue.c
  1. #include <stdio.h>

  2. #include "queue.h"

  3. typedef struct _queue_t
  4. {
  5.     char    a;  
  6.     char    b;  
  7. } queue_t;

  8. queue_t *create_queue()
  9. {
  10.     queue_t *q=NULL;

  11.     printf("create queue.\n");
  12.     q = (queue_t *)malloc(sizeof(queue_t));

  13.     return q;
  14. }

  15. void destory_queue(queue_t *q)
  16. {
  17.     printf("destory queue\n");
  18.     free(q);
  19. }
复制代码
main.c
  1. #include <stdio.h>
  2. #include "queue.h"

  3. int main()
  4. {
  5.     queue_t *q=create_queue();

  6.     printf("q=%x\n", q);

  7.     destory_queue(q);

  8.     return 0;
  9. }
复制代码

出0入0汤圆

发表于 2012-11-9 17:06:06 | 显示全部楼层
jameszxj 发表于 2012-11-9 16:32
首先声明和定义是有区别的,至于我的想法,我是实践过的,可能表述的不太明确,要用文字来表述明白还是蛮 ...

你这种方式,只有.c文件对编译器可见的时候才有效。如果你要给别人提供的是一个.lib文件和一个.h文件呢?

如果都把.c文件提供给人家了,那还谈论什么封装、谈什么隐藏呢?

出0入296汤圆

 楼主| 发表于 2012-11-9 18:08:59 | 显示全部楼层
本帖最后由 Gorgon_Meducer 于 2012-11-9 18:20 编辑
jameszxj 发表于 2012-11-9 16:32
首先声明和定义是有区别的,至于我的想法,我是实践过的,可能表述的不太明确,要用文字来表述明白还是蛮 ...


你这个方法有一个很致命的问题……
结构体缺乏大小信息,别人没办法根据你的头文件来进行malloc操作。
而且,如果这个代码能工作,则说明别人也是知道结构体的内部信息的。

在单纯使用你的头文件的情况下,因为缺乏大小信息,你无法用新定义的类型制作成数组——做成指针数组是可以的。
变量具有三要素
1、起始地址——本质上就是一个整数
2、大小
3、操作集(能进行什么样的操作)

C语言的特性允许你只提供三要素的一个要素,也就是你头文件用的方法。第三个要素是我们需要隐藏的,这没有问题。
但是对于需要用到第二要素的场合,比如建立数组,比如将类型作为别的结构体的元素,比如在外部对指向这个类型的指针进行加减运算,都会报告错误。
另外,你不能替别人决定数据类型是从heap来的还是从静态分配来的。很多嵌入式环境根本不允许用 malloc。在可以使用malloc的环境,由于malloc算法
会导致heap存在碎片,从而降低内存的利用率。比较常见的替代方法是,自己通过链表的方法为指定的类型建立专用堆,说白了就是用目标类型建立一个
数组,然后利用freelist指针将这些数组元素串起来……无论怎样,都离不开三要素的第二要素。

所以,从信息隐藏的角度来说,是得不偿失的。


类似的方法有
.c里面建立了一个二维数组,比如
  1. uint8_t g_chArray[3][2];
复制代码
在.h里面放置了缺乏第二信息要素的声明
  1. extern uint8_t g_chArray[][2];
复制代码
这样的代码,在外面用sizeof(g_chArray)会导致编译器报错,但其他不牵涉大小的应用场合则是没有问题的。这也是C语言灵活的地方之一。

出0入0汤圆

发表于 2012-11-10 11:46:07 | 显示全部楼层
傻孩子 出马,果然 非同一般啊;
研究中。。。。

出0入0汤圆

发表于 2012-11-10 11:46:30 | 显示全部楼层
傻孩子 出马,果然 非同一般啊;
研究中。。。。

出0入0汤圆

发表于 2012-11-10 11:49:51 | 显示全部楼层
受教了,占位学习

出0入0汤圆

发表于 2012-11-10 12:06:30 | 显示全部楼层
这个必须顶一下

支持傻孩子,在您的帖子中学到了不少,我最近也一直在思考用C写模拟的面向对象

确实OO只是思想和语言没有关系。

在这以前我一直觉得C可以写OO,继承也没有问题,多重继承这种鸡肋就算了

但解决不了,private成员保护的问题,不知这篇文章能否改变我的观点

好了,开始看文章

出0入296汤圆

 楼主| 发表于 2012-11-10 13:37:15 | 显示全部楼层
pang123hui 发表于 2012-11-10 12:06
这个必须顶一下

支持傻孩子,在您的帖子中学到了不少,我最近也一直在思考用C写模拟的面向对象

OOPC是很成熟的技术了,国外的文献和理论很多。不过与别的技术不同,OOPC除了应用以外,对工程师来说最大的乐趣
就是“重复发明一次轮子”——这也是中国环境下少见的苦中作乐的机会了。
加油,有啥问题可以一起讨论。

B.T.W 难得碰到一个Kanon的名雪党,这个要顶起。

出0入0汤圆

发表于 2012-11-10 15:02:36 | 显示全部楼层
有能力,弄个操作系统来

出0入0汤圆

发表于 2012-11-10 18:43:10 | 显示全部楼层
Gorgon_Meducer 发表于 2012-11-10 13:37
OOPC是很成熟的技术了,国外的文献和理论很多。不过与别的技术不同,OOPC除了应用以外,对工程师来说最大 ...

哈哈,Air、Kanon、Clannad可是我高中的最爱哦,我今年刚大学毕业,

最喜欢名雪了,觉得高三的时候桌子上面放的就是名雪的相框。


恩,我一直坚信,单片机软件工程师也是需要学习数据结构、算法和软件工程概念的,虽然貌似对于单片机软件工程师都很淡化这些。

最近我也在一直尝试着提高代码的重用率,开始把一些常用的模组写成Lib,比如最近刚写了CRC,AES的lib,以后只需要维护这个lib就可以了,不用像以前一样,

好几个项目里有AES的需求,结果代码都不一样,有细微差别,有时候想改,不得不把每个工程都改一下,时间一长必然出错,吃了大亏

现在开始Lib的一劳永逸,哈哈

还有,期待你的新书啊,上一版我买了,但感觉比较浅显一些,只是把状态机的部分看了,我看你的签名介绍后,很期待你的新书哦,出版必须买一本

出0入0汤圆

发表于 2012-11-10 20:29:15 | 显示全部楼层
eduhf_123 发表于 2012-11-9 17:06
你这种方式,只有.c文件对编译器可见的时候才有效。如果你要给别人提供的是一个.lib文件和一个.h文件呢? ...

你这种说法是错误的,这种当然适用于提供lib库,你可以自己试验一下。

出0入0汤圆

发表于 2012-11-10 20:49:22 | 显示全部楼层
Gorgon_Meducer 发表于 2012-11-9 18:08
你这个方法有一个很致命的问题……
结构体缺乏大小信息,别人没办法根据你的头文件来进行malloc操作。
而 ...

关于大小的问题,我在上面的回复已经说明了,这确实是一个问题,但是我认为这并不是致命问题。
我觉得这种封装性更好,更适合用于提供库文件的方式,至于一些嵌入式系统中不能使用malloc,那自然不能使用这种方法,你的方法更适合。

有一点,我觉得你的方法还是公开了结构体内容。

出0入296汤圆

 楼主| 发表于 2012-11-10 21:23:32 | 显示全部楼层
jameszxj 发表于 2012-11-10 20:49
关于大小的问题,我在上面的回复已经说明了,这确实是一个问题,但是我认为这并不是致命问题。
我觉得这 ...

凡事不可太尽,否则缘必早尽……
我保留意见。

我讨论的原则就是要引出各种声音,让大家都能吸收各家之言,无高下对错之分。
谢谢你的方案。

出0入0汤圆

发表于 2012-11-10 23:20:18 | 显示全部楼层
支持牛人出教程。amo的网站的人气都是靠牛人给撑起来的。

出0入0汤圆

发表于 2012-11-14 13:11:43 | 显示全部楼层
从来都是看帖子不回帖的我,今天忏悔来了,因为没有,积分,发帖子都不让用链接呀。

出0入0汤圆

发表于 2012-11-15 01:56:09 | 显示全部楼层
jameszxj 发表于 2012-11-10 20:29
你这种说法是错误的,这种当然适用于提供lib库,你可以自己试验一下。

malloc不是已经不可以了么?

出0入0汤圆

发表于 2012-11-15 02:04:59 | 显示全部楼层
jameszxj 发表于 2012-11-10 20:49
关于大小的问题,我在上面的回复已经说明了,这确实是一个问题,但是我认为这并不是致命问题。
我觉得这种封装性更好,更适合用于提供库文件的方式,至于一些嵌入式系统中不能使用malloc,那自然不能使用这种方法,你的方法更适合。

有一点,我觉得你的方法还是公开了结构体内容。


是公开了结构体内容,但这并不意味着也同时公开了结构体的存储细节——“类”的各个成员在.c文件中定义的顺序并不一定要和在.h文件中声明的顺序一致,在向用户公开了“类”的大小、公开了“类”的成员的同时,明确地告诉用户不能野蛮地使用指针的方式来直接访问成员——这样不是更符合我们“封装”的初衷么:你知道它是什么、你知道它是什么样的、但是你就是不能私自按照你的意愿去篡改它。

出0入0汤圆

发表于 2012-11-15 02:26:50 | 显示全部楼层
jameszxj 发表于 2012-11-10 20:49
关于大小的问题,我在上面的回复已经说明了,这确实是一个问题,但是我认为这并不是致命问题。
我觉得这种封装性更好,更适合用于提供库文件的方式,至于一些嵌入式系统中不能使用malloc,那自然不能使用这种方法,你的方法更适合。

有一点,我觉得你的方法还是公开了结构体内容。

而且,在非嵌入式平台,资源不严格受限的情况下,LZ的方式同样有个巨大的优点是你的方法无法做到的:利用我在49楼的回复的思路,LZ的方式可以很容易地给这个C语言中的“类”增加this指针、static成员、等等。

出0入0汤圆

发表于 2012-11-15 08:24:45 | 显示全部楼层
楼主高人,向你学习,谢谢

出10入10汤圆

发表于 2012-11-15 08:27:16 | 显示全部楼层
新书预热,支持

出0入0汤圆

发表于 2012-11-15 09:32:56 | 显示全部楼层
代码顶楼傻孩子已有,我这里只是为了帮助理解做了必要的解释

(一)首先基础知识
C语言中 ## 宏的用法,表示连接两个字符,
#define CLASS(__NAME)               __##__NAME
则就是将 __NAME 和之前的字符(这里是 __)连接起来,去掉##
例如
CLASS(queue_t)  实际上替换出来的结果是 __queue_t

(二)对于 queue.h 注意,这个文件不参与自己的库的编译,这个头文件是提供给用户的。
头文件定义一个类
EXTERN_CLASS
    uint8_t *pchBuffer;    //!< queue buffer   
    uint16_t hwBufferSize; //!< buffer size
    uint16_t hwHead;       //!< head pointer
    uint16_t hwTail;       //!< tail pointer
    uint16_t hwCounter;    //!< byte counter
END_EXTERN_CLASS(queue_t)

那么根据宏扩展得到的结果如下:
typedef struct {
    uint8_t chMask[sizeof(struct {
    uint8_t *pchBuffer;    //!< queue buffer   
    uint16_t hwBufferSize; //!< buffer size
    uint16_t hwHead;       //!< head pointer
    uint16_t hwTail;       //!< tail pointer
    uint16_t hwCounter;    //!< byte counter
    })];
}queue_t;

从代码中可以看出,这里其实只是定义了一个普通的字节数组(unsigned char)
你可以简单的理解为一块缓冲,用户的角度看到的只是一块缓冲区,缓冲区的大小是这个结构体本身的大小。


(三)内部的库怎么做 queue.c 这个是自己的代码,lib形式发布,不对用户开放。
库中定义了这么一个类型
DEF_CLASS
    uint8_t *pchBuffer;    //!< queue buffer  
    uint16_t hwBufferSize; //!< buffer size  
    uint16_t hwHead;       //!< head pointer
    uint16_t hwTail;       //!< tail pointer
    uint16_t hwCounter;    //!< byte counter
END_DEF_CLASS(queue_t)

根据宏定义扩展之后得到
typedef struct {
    uint8_t *pchBuffer;    //!< queue buffer  
    uint16_t hwBufferSize; //!< buffer size  
    uint16_t hwHead;       //!< head pointer
    uint16_t hwTail;       //!< tail pointer
    uint16_t hwCounter;    //!< byte counter
}__queue_t;
typedef struct {
    uint8_t chMask[sizeof(__queue_t)];
}queue_t;

第一,因为queue.c 并不包含 queue.h ,所以不会产生类型定义冲突。它定义了一个内部使用的结构体 __queue_t
(注:通常 __ 标示的变量都是系统变量)

(四)库内部怎么操作
bool is_queue_empty(queue_t *ptQueue)
{
//    CLASS(queue_t) *ptQ = (CLASS(queue_t) *)ptQueue;
进行宏替换之后其实是这样的
    __queue_t *ptQ = (__queue_t *)ptQueue;

    if (NULL == ptQueue) {
        return true;
    }
    return ((ptQ->hwHead == ptQ->hwTail) && (0 == ptQ->Counter));
}
...
可以看到,在库内部使用的其实就是  __queue_t 类型,这个对于外部的用户来说是不可见的,因为queue.h中根本
没有定义这个类型。
但是(重点)__queue_t 结构体和 uint_t 数组 chMask 其实指的是同一块内存。

可以这么理解,用户定义一块缓冲区传递给lib库,而库则将这块缓冲区看成是  __queue_t 结构体完成各种操作

用户只能通过接口函数访问内部,用户对结构体的操作是非法的(因为对于用户来说,它只是定义了一个 uint8_t 数组
并不是结构体,所以不能对结构体进行操作。
通过操作函数
extern bool is_queue_empty(queue_t *ptQueue);

好处是保护了lib私有的变量。

出0入296汤圆

 楼主| 发表于 2012-11-15 10:57:35 | 显示全部楼层
Etual 发表于 2012-11-15 09:32
代码顶楼傻孩子已有,我这里只是为了帮助理解做了必要的解释

(一)首先基础知识

非常感谢您的注解,解释的非常到位。

出0入296汤圆

 楼主| 发表于 2012-11-15 10:59:35 | 显示全部楼层
eduhf_123 发表于 2012-11-15 02:26
而且,在非嵌入式平台,资源不严格受限的情况下,LZ的方式同样有个巨大的优点是你的方法无法做到的:利用 ...

没错,除了this指针,还可以加入base结构……也就是传说中的继承……

出0入0汤圆

发表于 2012-11-15 11:05:23 | 显示全部楼层
学习。。。。

出0入0汤圆

发表于 2012-11-15 13:46:18 | 显示全部楼层
Gorgon_Meducer 发表于 2012-11-15 10:59
没错,除了this指针,还可以加入base结构……也就是传说中的继承……

是的,这里的关键是能加东西了,即具备可扩展性了,具体加什么,那就任由你发挥了。
基本上,你想加什么都是可以的了——只要你敢想,无非就是花多大代价的问题。

出0入0汤圆

发表于 2012-11-15 21:45:50 | 显示全部楼层
学习

出0入0汤圆

发表于 2012-11-15 22:53:54 | 显示全部楼层
傻孩子回归

好东西不少

出0入0汤圆

发表于 2012-11-25 16:07:07 | 显示全部楼层
在CSDN上闲逛,发现了这个帖,很有意思,可以参考。

出0入296汤圆

 楼主| 发表于 2012-11-26 00:21:54 | 显示全部楼层
eduhf_123 发表于 2012-11-25 16:07
在CSDN上闲逛,发现了这个帖,很有意思,可以参考。

面向对象是个思想,纠结OOC与C++的形似就变成舍本求末了。这篇帖子只讲保护,不讲OOPC
更不去追求什么形似。OOPC看台湾人的树就足够了。剩下就是怎么用的问题啦。

出0入8汤圆

发表于 2012-11-26 03:08:26 | 显示全部楼层
搬个板凳 好好学习

出0入0汤圆

发表于 2012-11-26 08:26:42 | 显示全部楼层
这个要mark下

出0入0汤圆

发表于 2012-12-4 11:42:47 | 显示全部楼层
请傻孩子推荐几本书吧,关于用C语言实现面向对象方面的书,很想在这方面钻研下!!!谢谢

出0入296汤圆

 楼主| 发表于 2012-12-4 12:10:57 | 显示全部楼层
qufuta 发表于 2012-12-4 11:42
请傻孩子推荐几本书吧,关于用C语言实现面向对象方面的书,很想在这方面钻研下!!!谢谢 ...

UML+OOPC或者你搜索OOC

出0入93汤圆

发表于 2012-12-4 13:42:20 | 显示全部楼层
不对,隐藏完全是可能的,声明和定义不一致就可以了。面向对象是思想,实现的方式可以多种多样,继承、派生都可以。

queue.h

  1. #ifndef __QUEUE_H__
  2. #define __QUEUE_H__

  3. typedef struct _queue_t                //相当于接口
  4. {
  5.     char    a;                         //用户可以看见a,b,相当于public访问权限
  6.     char    b;  
  7. } queue_t;

  8. queue_t *create_queue();
  9. void destroty_queue(queue_t *q);

  10. #endif /* __QUEUE_H__ */
复制代码
queue.c

  1. #include <stdio.h>
  2. #include "queue.h"

  3. typedef struct _queue_impl_t                               //派生,实际使用的都是这个类型
  4. {
  5.         queue_t q;
  6.         int f;                                                        //私有的,不允许.h文件访问
  7. }queue_impl_t;

  8. queue_t *create_queue()
  9. {
  10.     queue_impl_t *q=NULL;

  11.     printf("create queue.\n");
  12.     q = (queue_impl_t *)malloc(sizeof(queue_impl_t));
  13.     q -> f = 123;                                        //queue_t无法访问的

  14.     return (queue_t*)q;
  15. }

  16. void destory_queue(queue_t *q)
  17. {
  18.     printf("destory queue\n");
  19.     free((queue_impl_t*)q);
  20. }
复制代码
main.c

  1. #include <stdio.h>
  2. #include "queue.h"

  3. int main()
  4. {
  5.     queue_t *q=create_queue();
  6.     printf("q=%x\n", q);
  7.     destory_queue(q);
  8.     return 0;
  9. }
复制代码

出0入93汤圆

发表于 2012-12-4 13:47:29 | 显示全部楼层
Windows API中的结构体中,我不相信微软就只用了那几个成员,更不相信结构体还分STRUCT、STRUCT_EX、STRUCT_EX_EX、...之类的,更别说结构体中N多的Reserved了,各种奇怪的变长结构……变长结构中,他真的什么细节都没有封装?

出0入0汤圆

发表于 2012-12-4 14:30:18 | 显示全部楼层
受教了,占位学习

出0入70汤圆

发表于 2012-12-4 21:13:53 | 显示全部楼层
Mark C 封装
期待下集

出0入296汤圆

 楼主| 发表于 2012-12-5 10:26:47 | 显示全部楼层
takashiki 发表于 2012-12-4 13:42
不对,隐藏完全是可能的,声明和定义不一致就可以了。面向对象是思想,实现的方式可以多种多样,继承、派生 ...

这种方式同样限定了外部不能使用对应类型的大小信息。

出0入0汤圆

发表于 2012-12-5 10:43:57 | 显示全部楼层
以前也看过面向对象的思想,不过一直都没弄明白为什么要这么做,现在明白了,多谢楼主,讲得这么透彻。

出0入93汤圆

发表于 2012-12-5 10:58:11 | 显示全部楼层
本帖最后由 takashiki 于 2012-12-5 11:03 编辑
Gorgon_Meducer 发表于 2012-12-5 10:26
这种方式同样限定了外部不能使用对应类型的大小信息。


如果需要使用对应类型的大小信息,那就域中弄上一大堆的Reserved。
比如Windows API中,关于串行通信的部分(串口配置):
  1. typedef struct_COMM_CONFIG {
  2.     DWORD dwSize;
  3.     WORD  wVersion;  
  4.     WORD  wReserved;                                                     //这个地方允许用户随便更改吗?负责任的程序员一定不会去试图胡乱更改的,不负责任的和NX的除外,Ring0 API都可以Hook,这点东西就算不了什么了。
  5.     DCB   dcb;
  6.     DWORD dwProviderSubType;
  7.     DWORD dwProviderOffset;
  8.     DWORD dwProviderSize;
  9.     WCHAR wcProviderData[1];
  10. } COMMCONFIG, *LPCOMMCONFIG;
复制代码
在Windows COM的世界里,大量的采用interface,interface在标准C++中,对应于所有方法都公开的、所有方法都是纯虚方法的纯虚类。我们在使用时,同样只要拿来用就行了,根本用不着查看他的大小到底是多少。面向对象要进行封装,至于我是否全部封装后都显现给你还是封装后只显现一部分用户完全不需要去理会的。

如果一定需要得到对应类型的大小,那其实很简单,再导出一个函数就是了。
比如:
  1. int queue_get_size(queue_t *)
  2. {
  3.     return sizeof queue_impl_t;
  4. }
复制代码
不就什么事都没有了,既然作为API提供了create_***、destory_***,我就是不希望用户自己随便去malloc,细节完全由API进行封装。

出100入0汤圆

发表于 2012-12-5 11:17:42 | 显示全部楼层
Mark 学习

出0入0汤圆

发表于 2012-12-5 11:22:41 | 显示全部楼层
mark,流明。

出0入296汤圆

 楼主| 发表于 2012-12-5 11:52:26 | 显示全部楼层
本帖最后由 Gorgon_Meducer 于 2012-12-5 12:00 编辑
takashiki 发表于 2012-12-5 10:58
如果需要使用对应类型的大小信息,那就域中弄上一大堆的Reserved。
比如Windows API中,关于串行通信的部 ...


支持private和public混搭的掩码结构体技术我这里也是有的,但是我后来决定放弃了:
原因很简单,长期C#开发的经验(100W行)告诉我,public的成员变量是没有用的,需要public的成员
一定可以用property来处理(可以在set和get方法里面处理线程安全问题)。

因此,我决定用这样简洁的全private封装策略。

我们一方面不希望用户来mallc,但是你也不能替用户确定用什么方式来分配资源。用户根据情况可能
会选择静态分配,也可能会用数组(小堆),也可能会构建专用堆(大堆),这些都需要大小信息。
用函数返回大小是一种方式,我也用过,但是我觉得用类型本身的大小信息是比较自然和优雅的。如
果你非要用public的成员变量,我宁可用public和private混搭的掩码结构体,因为这种结构下,大小
信息也是能得到保障的。

其实我这种所谓的封装技术,本质上只是一种洁癖的体现,没有绝对的理由去支持它,或者否定别的做法。
好在同样有洁癖的人还是有的,所以不必抓住这种技术的合理性不放。Style而已~娱乐下

出0入0汤圆

发表于 2012-12-5 13:57:22 | 显示全部楼层
Gorgon_Meducer 发表于 2012-12-5 11:52
支持private和public混搭的掩码结构体技术我这里也是有的,但是我后来决定放弃了:
原因很简单,长期C#开发的经验(100W行)告诉我,public的成员变量是没有用的,需要public的成员
一定可以用property来处理(可以在set和get方法里面处理线程安全问题)。

因此,我决定用这样简洁的全private封装策略。

我们一方面不希望用户来mallc,但是你也不能替用户确定用什么方式来分配资源。用户根据情况可能
会选择静态分配,也可能会用数组(小堆),也可能会构建专用堆(大堆),这些都需要大小信息。
用函数返回大小是一种方式,我也用过,但是我觉得用类型本身的大小信息是比较自然和优雅的。如
果你非要用public的成员变量,我宁可用public和private混搭的掩码结构体,因为这种结构下,大小
信息也是能得到保障的。

其实我这种所谓的封装技术,本质上只是一种洁癖的体现,没有绝对的理由去支持它,或者否定别的做法。
好在同样有洁癖的人还是有的,所以不必抓住这种技术的合理性不放。Style而已~娱乐下


专门来顶最后那段蓝色字。

出0入296汤圆

 楼主| 发表于 2012-12-5 14:05:26 | 显示全部楼层
eduhf_123 发表于 2012-12-5 13:57
专门来顶最后那段蓝色字。

难得自己吐自己嘈……你这都不放过……

出0入0汤圆

发表于 2012-12-6 11:02:04 | 显示全部楼层
Gorgon_Meducer 发表于 2012-12-4 12:10
UML+OOPC或者你搜索OOC

好的,感谢你的推荐,希望以后能跟你多交流,多指教!!!

出0入0汤圆

发表于 2012-12-6 14:03:15 | 显示全部楼层
Gorgon_Meducer 发表于 2012-12-5 14:05
难得自己吐自己嘈……你这都不放过……

没有啊,我就也是你所说的“同样有洁癖的人”啊。

出0入296汤圆

 楼主| 发表于 2012-12-6 15:10:15 | 显示全部楼层
qufuta 发表于 2012-12-6 11:02
好的,感谢你的推荐,希望以后能跟你多交流,多指教!!!

多交流~

出0入0汤圆

发表于 2012-12-9 17:14:45 | 显示全部楼层
Etual 发表于 2012-11-15 09:32
代码顶楼傻孩子已有,我这里只是为了帮助理解做了必要的解释

(一)首先基础知识

这种所谓封装严格来说和直接定义差不多。

头文件中定义
struct sth_private{
    char data[sizeof(struct{int a, int b})];
};

用户文件中定义:
struct sth_private  v;

v.a = 2;
v.b = 3;    //error, 结构体没有这样的成员

用户虽然不能直接访问成员a和b了;但“不怀好意”的程序员完全可以这样

typedef struct _my_hack{
    int a;
    int b;
}my_hack;

struct sth_private v;
my_hack  *hack_of_v = (my_hack*)v;
hack_of_v->a = 2;
hack_of_v->b = 3;     // ok,my_hack拥有a,b成员,这样就绕过了“private”的限制了。

所以,c/c++这样基于值语意的程序设计语言,private,public都只是语言层面的一种约定。
用户和库作者遵循共同的契约。


出0入296汤圆

 楼主| 发表于 2012-12-9 17:21:28 | 显示全部楼层
ifree64 发表于 2012-12-9 17:14
这种所谓封装严格来说和直接定义差不多。

头文件中定义

1、可以等体积替换的
2、这种方法只是设立一个门槛,让正常的程序员明确知道这里是不应该
     访问的。就像C语言static的语法限定一样,只要是静态分配的,其实
     都有能力访问。
3、没有幻想可以阻止不怀好意的人。你提交模块的时候假设对方是遵守
     约定的,相应的测试也是建立在这个基础之上的。如果对方非法使用
     出了质量问题,就是他们的事情了。

出0入0汤圆

发表于 2012-12-9 17:36:55 | 显示全部楼层
mark下。。

出0入0汤圆

发表于 2012-12-9 20:02:24 | 显示全部楼层
Gorgon_Meducer 发表于 2012-12-9 17:21
1、可以等体积替换的
2、这种方法只是设立一个门槛,让正常的程序员明确知道这里是不应该
     访问的。 ...

objective-C里有一个功能叫做“分类”,才是比较完美的解决了类似的信息隐藏问题。它可以这样:

在头文件里:

@interface ObjectA
-(void) function1();
-(void) function2();
@end


在实现文件里声明一个ObjectA的分类,分类的成员头文件里根本不知道。
@interface ObjectA ()
{
    int member1;
    char member2;
}
@end

@implementation ObjectA
-(void) function1()
{

}

-(void) function2()
{

}
@end

当然,用户创建的Obj-C对象全都是指针,指向真正的对象,不能在栈里创建对象。

出0入0汤圆

发表于 2012-12-9 22:03:28 来自手机 | 显示全部楼层
好帖,学习中

出0入296汤圆

 楼主| 发表于 2012-12-9 23:00:14 | 显示全部楼层
ifree64 发表于 2012-12-9 20:02
objective-C里有一个功能叫做“分类”,才是比较完美的解决了类似的信息隐藏问题。它可以这样:

在头文 ...

Object-C 已经是地道的OO语言了,不在讨论之列

出0入0汤圆

发表于 2012-12-10 14:56:27 | 显示全部楼层
学习了 谢谢

出0入0汤圆

发表于 2013-1-23 10:58:28 | 显示全部楼层
很好的帖子,学习了

出0入0汤圆

发表于 2013-1-23 12:05:40 | 显示全部楼层
Gorgon_Meducer 发表于 2012-12-5 10:26
这种方式同样限定了外部不能使用对应类型的大小信息。

这句话不太懂,我在第三个文件中不是可以用  int size =sizeof( queue_t)来算对应类型的大小信息的吗?

出0入0汤圆

发表于 2013-1-23 12:18:11 | 显示全部楼层
做个标记,晚上回去看

出0入0汤圆

发表于 2013-1-23 12:18:36 | 显示全部楼层
名字改了?

出0入0汤圆

发表于 2013-1-23 14:46:30 | 显示全部楼层
拜读一下

出0入296汤圆

 楼主| 发表于 2013-1-24 02:19:42 | 显示全部楼层
imjacob 发表于 2013-1-23 12:05
这句话不太懂,我在第三个文件中不是可以用  int size =sizeof( queue_t)来算对应类型的大小信息的吗? ...

在.h不提供大小信息的情况下,使用sizeof会导致编译器报错

出0入0汤圆

发表于 2013-1-29 16:12:29 | 显示全部楼层
本帖最后由 imjacob 于 2013-1-29 16:13 编辑
Gorgon_Meducer 发表于 2013-1-24 02:19
在.h不提供大小信息的情况下,使用sizeof会导致编译器报错


我指的是65楼的写法是可以的,32楼写法是不可以的。而且我在VC2010里试了的,确实这样的。

  1. #include <stdio.h>
  2. #include "queue.h"

  3. int main()
  4. {
  5.     queue_t *q=create_queue();
  6.         int size = sizeof(queue_t);
  7.     printf("q=%x\n", q);
  8.     destory_queue(q);
  9.     return 0;
  10. }
复制代码
如上,size是可以得到的,在65楼的情况下

出0入296汤圆

 楼主| 发表于 2013-1-29 16:33:50 | 显示全部楼层
本帖最后由 Gorgon_Meducer 于 2013-1-29 16:39 编辑
imjacob 发表于 2013-1-29 16:12
我指的是65楼的写法是可以的,32楼写法是不可以的。而且我在VC2010里试了的,确实这样的。如上,size是可 ...


65楼里面有完整的大小信息,当然可以。只要编译器没有办法知道大小信息,sizeof就会报告错误。
但你这里还是有问题的,因为queue_t的大小和实际背后的大小是不同的,如果依赖这个大小信息
进行指针操作就会出现错误。因为.h里面没有提供任何暗示,则别人完全可以认为自己用queue_t
的大小作为大小是合法的。如果这种结构体不幸要通过序列化(转换成字节流)然后传输到某些
地方,比如通过消息机制在进程甚至是不同芯片间进行传递(不要局限在队列的例子上,我们讨论的
是一种通用的方法论),显然就会导致很严重的错误:在发送方没有完整传送,在接收方则会导致
内存入侵。

出0入0汤圆

发表于 2013-2-21 15:16:27 | 显示全部楼层
ifree64 发表于 2012-12-9 17:14
这种所谓封装严格来说和直接定义差不多。

头文件中定义

开了眼界了,这应该是黑客技术了吧

出0入0汤圆

发表于 2013-3-12 15:30:11 | 显示全部楼层
好帖啊。以后多关注

出0入0汤圆

发表于 2013-3-12 16:32:27 来自手机 | 显示全部楼层
学习一下....

出0入0汤圆

发表于 2013-3-14 14:50:01 | 显示全部楼层
很受用的贴子!

出0入0汤圆

发表于 2013-3-19 16:29:48 | 显示全部楼层
MARK!!

出0入0汤圆

发表于 2013-3-20 16:42:59 | 显示全部楼层
给跪了                     

出0入0汤圆

发表于 2013-3-20 16:51:56 | 显示全部楼层
怎么就能起到保护作用呢  举个试图改写里面内容的例子呗
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-26 19:25

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表