原文参考
在展开知识点之前,首先要重申两点:
1、面向对象是一种思想,基本与所用的语言是无关的。当你心怀面向对象,即便用QBasic也能写出符合面向对象思想的代码,
更不用说是C语言了。如很多人初学C++的时候,并没有掌握面向对象的思想,活生生把类当结构体来用的,也不在少数吧?
2、面向对象的最基本出发点是“将数据以及处理数据的方法封装在一起”。至于继承、派生、多态之类则是后面扩展的东西。
在C语言中,如果用结构体来保存数据,并将处理这些数据的函数与结构体的定义封装在同一个.c文件中,
则该.c文件就可以视作是一个类。如果将指向具体函数的函数指针与结构体的其它成员变量封装在同一个结构体中,
则该“对象”的使用甚至就与C++相差无几了。
以上内容是面向对象C语言(Object-Oriented C Programming with ANSI-C)技术的基本出发点。作为引子,
在使用OOC技术的时候,我们会遇到这么一个问题:是的,我们可以用结构体来模拟类,将所有的成员变量都放在结构体中,
并将这一结构体放置在类模块的接口头文件中(xxxx.h),但问题是,结构体里面的成员变量都是public的,
如何保护他们使其拥有private属性呢?解决的方法就是使用掩码结构体(Masted Structure)
那么,什么是掩码结构体呢?在回答这个问题之前,我们先看看下面这个例子。
已知我们定义了一些用于在C语言里面进行类封装的宏,如下所示:
#define EXTERN_CLASS typedef struct {\
uint8_t chMask[sizeof(struct {
#define END_EXTERN_CLASS(__NAME) })];\
}__NAME;
#define DEF_CLASS typedef struct {
#define END_DEF_CLASS(__NAME) }__##__NAME;\
typedef struct {\
uint8_t chMask[sizeof(__##__NAME)];\
}__NAME;
//##表示连接字符, 就是将 __NAME 和之前的字符(这里是 __)连接起来,去掉##
#define CLASS(__NAME) __##__NAME
假设我们要封装一个基于字节的队列类,不妨叫做Queue,因此我们建立了一个类文件queue.c
和对应的接口头文件queue.h。假设我们约定queue.c将不包含queue.h(这么做的好处很多,在以后的
内容里再讲解,当然对掩码结构体技术来说,模块的实现是否包含模块的接口头文件并不是关键)。
我们首先想到的是要定义一个类来表示队列,它的一个可能的形式如下
//! \name byte queue
//! @{
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;
//! @}
目前为止,一切都还OK。由于queue.c并不包含queue.h,因此我们决定在两个文件中各放一个定义。由于.h
中包含了队列的完整数据信息,使用该模块的人可能会因为种种原因直接访问甚至修改队列结构体中的数据
——也许在这个例子中不是那么明显,但是在你某个其它应用模块的例子中,你放在结构体里面的某个信息
可能对模块的使用者来说,直接操作更为便利,因此悲剧发生了——原本你假设“所有操作都应该由queue.c
来完成的”格局被打破了,使用者可以轻而易举的修改和访问结构体的内容——而这些内容在面向对象思想中
原本应该是私有的,无法访问的(private)。原本测试完好的系统,因为这种出乎意料的外界干涉而导致不稳
定,甚至是直接crash了。当你气冲冲的找到这么“非法”访问你结构体的人时,对方居然推了推眼镜一脸无辜
的看着你说“根据接口的最小信息公开原则,难道你放在头文件里面的信息不是大家放心可以用的么?”
OTZ...哑口无言,然后你会隐约觉得太阳穴微微的在跳动……
且慢,如果我们通过一开始提供的宏分别对queue.h和queue.c中的定义改写一番,也许就是另外一个局面了:
queue.h
...
//! \name byte queue
//! @{
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)
//! @}
...
extern bool queue_init(queue_t *ptQueue, uint8_t *pchBuffer, uint16_t hwSize);
extern bool enqueue(queue_t *ptQueue, uint8_t chByte);
extern bool dequeue(queue_t *ptQueue, uint8_t *pchByte);
extern bool is_queue_empty(queue_t *ptQueue);
...
queue.c
...
//! \name byte queue
//! @{
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_t是一个掩码结构体,里面只有一个起到掩码作用的数组chMask,其大小和真正后台的的类型__queue_t相同——
这就是掩码结构体实现私有成员保护的秘密。解决了私有成员保护的问题,剩下还有一个问题,对于queue.c的
函数来说queue_t只是一个数组,那么正常的功能要如何实现呢?下面的代码片断将为你解释一切:
...
bool is_queue_empty(queue_t *ptQueue)
{
CLASS(queue_t) *ptQ = (CLASS(queue_t) *)ptQueue;
if (NULL == ptQueue) {
return true;
}
return ((ptQ->hwHead == ptQ->hwTail) && (0 == ptQ->Counter));
}
...
从编译器的角度来说,这种从queue_t到 __queue_t 类型指针的转义是逻辑上的,并不会因此产生额外的代码,
简而言之,使用掩码结构体几乎是没有代价的——如果你找出了所谓的代价,一方面不妨告诉我,另一方面,
不妨考虑这个代价和模块的封装相比是否是可以接受的。
(一)首先基础知识
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私有的变量。
2)我觉得这种方法基本屏蔽了结构体的内容,麻烦的是要提供创建函数,因为调用者无法知道结构体大小。
queue.h
#ifndef __QUEUE_H__
#define __QUEUE_H__
struct _queue_t;
typedef struct _queue_t queue_t;
queue_t *create_queue();
void destroty_queue(queue_t *q);
#endif /* __QUEUE_H__ */
queue.c
#include
#include "queue.h"
typedef struct _queue_t
{
char a;
char b;
} queue_t;
queue_t *create_queue()
{
queue_t *q=NULL;
printf("create queue.\n");
q = (queue_t *)malloc(sizeof(queue_t));
return q;
}
void destory_queue(queue_t *q)
{
printf("destory queue\n");
free(q);
}
main.c
#include
#include "queue.h"
int main()
{
queue_t *q=create_queue();
printf("q=%x\n", q);
destory_queue(q);
return 0;
}
你这个方法有一个很致命的问题……
结构体缺乏大小信息,别人没办法根据你的头文件来进行malloc操作。
而且,如果这个代码能工作,则说明别人也是知道结构体的内部信息的。
在单纯使用你的头文件的情况下,因为缺乏大小信息,你无法用新定义的类型制作成数组——做成指针数组是可以的。
变量具有三要素
1、起始地址——本质上就是一个整数
2、大小
3、操作集(能进行什么样的操作)
C语言的特性允许你只提供三要素的一个要素,也就是你头文件用的方法。第三个要素是我们需要隐藏的,这没有问题。
但是对于需要用到第二要素的场合,比如建立数组,比如将类型作为别的结构体的元素,比如在外部对指向这个类型的指针进行加减运算,都会报告错误。
另外,你不能替别人决定数据类型是从heap来的还是从静态分配来的。很多嵌入式环境根本不允许用 malloc。在可以使用malloc的环境,由于malloc算法
会导致heap存在碎片,从而降低内存的利用率。比较常见的替代方法是,自己通过链表的方法为指定的类型建立专用堆,说白了就是用目标类型建立一个
数组,然后利用freelist指针将这些数组元素串起来……无论怎样,都离不开三要素的第二要素。
所以,从信息隐藏的角度来说,是得不偿失的。
类似的方法有: .c里面建立了一个二维数组,比如 uint8_t g_chArray[3][2];
在.h里面放置了缺乏第二信息要素的声明 extern uint8_t g_chArray[][2];
这样的代码,在外面用sizeof(g_chArray)会导致编译器报错,但其他不牵涉大小的应用场合则是没有问题的。这也是C语言灵活的地方之一。
阅读(4533) | 评论(0) | 转发(1) |