C 语言不完全类型是什么?有什么用途?

1、不完全类型的概念

ISO(国际标准化组织(International Standard Organization))将 C 语言分为三个不同类型集合: 函数类型、对象类型和不完全类型,具体说明如下所示:

  • 函数类型:函数就是 C 语言的模块、一系列 C 语句的集合,有较强的独立性,能完成某个特定的功能,可以相互调用;
  • 对象类型:C 语言的对象类型不是说面向对象编程,而是在内存中创建具有特定长度,有意义的类型,例如 char、int、数组、结构体、指针等;
  • 不完全类型:不完全类型是指除了函数类型之外,大小不能被确定的类型。比如,声明了一个数组,但不给出数组的长度;声明了一个结构类型,但不给出结构体的定义,只告诉编译器这是一个结构体。在最终你还是必须得给出完整的定义,否则编译器在编译单元中都找不到不完全类型的完整定义信息的话就会报错。

C 语言所有数据类型如下图所示:

在 C99 标准中对不完全类型描述如下:

The void type comprises an empty set of values; it is an incomplete type that cannot be completed. (C99 6.2.5/19)An array type of unknown size is an incomplete type. It is completed, for an identifier of that type, by specifying the size in a later declaration (with internal or external linkage). A structure or union type of unknown content (as described in 6.7.2.3) is an incomplete type. It is completed, for all declarations of that type, by declaring the same structure or union tag with its defining content later in the same scope.(C99 6.2.5/22)

总结讲,C/C++中不完全类型有三种不同形式:void、未指定长度的数组以及具有非指定内容的结构和联合。void 类型与其他两种类型不同,因为它是无法完成的不完全类型,并且它用作特殊函数返回和参数类型。

不完全类型是暂时没有完全定义好的类型,编译器不知道这种类型该占几个字节的存储空间,例如以下定义方式:

int str[]; //不完全类型数组str定义
 
…
 
int str[10]; //定义str数组完整的类型信息

再举个例子,在头*.h 文件中声明结构:typedef struct __list *list_t;,最终在*.c 文件中定义如下:

struct __list {
 
    struct __list *prev;
 
    struct __list *next;
 
    viud   *data;
 
};

此时,结构体变量*list_t 就属于不完全类型,不完全类型不包含具体的类型信息,所以在未完整定义前不能通过 sizeof 来获知大小,并且不完全类型定义不适合局部变量。

2、不完全类型的用途

不完全类型的用途主要为以下三点:

1、提高代码灵活性。在*.h 头文件中声明的数组,不清楚具体使用场景应该需要多大,在*.c 中使用数组前再完整定义,就可以很方便的更改数组的大小,也不用再去修改头文件。

2、两个结构体需要相互指向,唯一能够实现的方式就是不完全结构,如下所示:

struct a { struct b *pb; };
 
struct b { struct a *pa; };

3、实现抽象模型的封装,降低程序模块之间的耦合,防止用户直接访问结构成员,破坏内部抽象数据类型。这样可以强制用户通过接口规则访问,隐藏内部实现细节,降低沟通成本。

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;         
};

从此之后,内部的细节都被隐藏了,封装成库之后别人再也不清楚内部的数据结构了,只能严格按照接口的要求进行调用,自然无法修改你的内部数据了。并且,以后修改内部实现也更方便了,甚至外部的接口都不需要做更改。

从用户的角度,知道的细节越少越好,即减少了记忆的成本,也避免了一些不必要的麻烦。


本文参考:麦克泰技术文章。

本文作者: InfoQ

余下全文(1/3)
分享这篇文章:

请关注我们:

共有 1 条讨论

  1. admin  这篇文章, 并对这篇文章的反应是俺的神呀赞一个

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注