不完整类型的用途
1、提高代码灵活性。在*.h头文件中声明的数组,不清楚具体使用场景应该需要多大,在*.c中使用数组前再完整定义,就可以很方便的更改数组的大小,也不用再去修改头文件。
2、两个结构体需要相互指向,唯一能够实现的方式就是不完全结构,例如:
struct a { struct b *pb; };
struct b { struct a *pa; };
3、实现抽象模型的封装,降低程序模块之间的耦合,防止用户直接访问结构成员,破坏内部抽象数据类型。这样可以强制用户通过接口规则访问,隐藏内部实现细节,降低沟通成本。
下面有一个小故事。
项目中需要用到环形缓存,于是小伙伴将这个任务交给了你。然后你实现了ring_buffer.c,并在ring_buffer.h头文件中定义了实现功能用的数据结构和接口:
typedef struct _ring_buffer_type
{
uint8_t *phead;
uint8_t *ptail;
uint8_t *pread;
uint8_t *pwrite;
size_t size;
volatile size_t counts;
}rcb_t;
/* 构建并初始化一个环形缓存 */
err_t ring_buffer_init(uint8_t *pbuffer, size_t size);
/* 向缓存中写数据 */
err_t ring_buffer_write(rcb_t *const p_rcb, uint8_t *pdata, size_t len);
/* 从缓存中读数据 */
err_t ring_buffer_read(rcb_t *const p_rcb, uint8_t *pdata, size_t len);
/* 检查缓存已使用的字节数 */
err_t ring_buffer_check(rcb_t *const p_rcb, size_t *len);
经过测试,功能实现很好,任务顺利完成。为了屏蔽功能实现细节你将模块封装成了库,信心十足的交给了小伙伴使用。但是你的伙伴却投来了鄙视的目光,说你的实现的功能有问题,于是你们一起检查他的代码,你发现他写了如下代码。
ring_buffer_write(&buf_rcb, pdata, 10);
buf_rcb. pwrite += 10;
buf_rcb.counts += 10;
于是你不解的质问小伙伴,为什么要动内部的数据,但小伙伴却说,往里面写入了数据,应该要修改指针啊。你认为的事,小伙伴想的却不一样。
然后为了不让别人动你内部的数据,于是你在头文件ring_buffer.h中把结构定义改成了:
typedef struct _ring_buffer_type rcb_t;
并将结构的定义放在了ring_buffer.c中:
struct _ring_buffer_type
{
uint8_t *phead;
uint8_t *ptail;
uint8_t *pread;
uint8_t *pwrite;
size_t size;
volatile size_t counts;
};
从此之后,内部的细节都被隐藏了,封装成库之后别人再也不清楚内部的数据结构了,只能严格按照接口的要求进行调用,自然无法修改你的内部数据了。并且,以后修改内部实现也更方便了,甚至外部的接口都不需要做更改。
从用户的角度,知道的细节越少越好,即减少了记忆的成本,也避免了一些不必要的麻烦。