C语言的本质(14)——不完全类型和复杂声明


ISO 将 C 的类型分为三个不同的集合: 函数、对象和不完全类型三大类。函数类型很明显;对象类型包含其他一切,除非不知道对象的大小。该标准使用术语“对象类型”指定指派的对象必须具有已知大小,注意,除 void 之外的不完全类型也称为对象。

对象类型又分为标量类型和非标量类型。枚举类型、字符型和各种整数的表示形式都被称作标量类型。当在C表达式中使用标量类型的值时,编译器就会自动将这些标识符转换为整数保存。这种机制的作用是,在这些标量类型上执行的操作与整型上执行的操作完全一样。指针类型属于标量类型,因此也可以做逻辑与、或、非运算的操作数和if、for、while的控制表达式,NULL指针表示假,非NULL指针表示真。

下图是C语言所有的数据类型:

 

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

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

 

struct s;
union u;
char str[];

 

数组元素、结构或联合的成员以及函数的局部对象都必须是对象类型的声明。其他情况的声明允许不完全类型。对于不完全类型,可以有下面的形式:

  • 指向不完全类型的指针
  • 返回不完全类型的函数
  • 不完全函数参数类型
  • 不完全类型的 typedef 名称

 除 void 之外,不完全类型在定义或调用函数之前,必须声明它。返回类型 void 指定不返回值的函数,单个参数类型 void 指定不接受参数的函数。

 由于数组和函数的参数类型重写为指针类型,因此表面看上去不完全的数组参数类型实际上并不是不完全类型。Main函数的 argv 的典型声明(即 char *argv[],一个未指定长度的字符指针数组)将被转化为一个指向字符指针的指针。

 具有不完全类型的变量可以通过多次声明组合成一个完全类型,比如数组str声明:

 

char str[];
char str[10];


当编译器碰到第一个声明时,认为str是一个不完全类型,碰到第二个声明时str就组合成完全类型了,如果编译器处理到程序文件的末尾仍然无法把str组合成一个完全类型,就会报错。读者可能会想,这个语法有什么用呢?为何不在第一次声明时就把str声明成完全类型?有些情况下这么做有一定的理由,比如第一个声明是写在头文件里的,第二个声明写在.c文件里,这样如果要改数组长度,只改.c文件就行了,头文件可以不用改。

 除了 void 之外,C 没有其他方法可处理不完全类型:结构和联合的前向引用。如果两个结构需要相互指向的指针,则唯一的方法是使用不完全类型,例如:

 

struct a { struct b *bp; };
struct b { struct a *ap; };

 

不完全的结构体类型的重要作用:

 有下列代码:

struct s {
         structt *pt;
};
 
struct t {
         structs *ps;
};

struct s和struct t各有一个指针成员指向另一种类型。编译器从前到后依次处理,当看到struct s { struct t* pt; };时,认为struct t是一个不完全类型,pt是一个指向不完全类型的指针,尽管如此,这个指针却是完全类型,因为不管什么指针都占4个字节存储空间,这一点很明确。然后编译器又看到struct t { struct s *ps; };,这时struct t有了完整的定义,就组合成一个完全类型了,pt的类型就组合成一个指向完全类型的指针。由于struct s在前面有完整的定义,所以struct s *ps;也定义了一个指向完全类型的指针。

 这样的类型定义是错误的:

 

struct s {
         structt ot;
};
 
struct t {
         structs os;
};

 编译器看到struct s { struct t ot; };时,认为struct t是一个不完全类型,无法定义成员ot,因为不知道它该占几个字节。所以结构体中可以递归地定义指针成员,但不能递归地定义变量成员,你可以设想一下,假如允许递归地定义变量成员,struct s中有一个struct t,struct t中又有一个struct s,struct s又中有一个struct t,这就成了一个无穷递归的定义。

 以上是两个结构体构成的递归定义,一个结构体也可以递归定义:

 

struct s {
         chardata[6];
         structs* next;
};

 

当编译器处理到第一行struct s {时,认为struct s是一个不完全类型,当处理到第三行struct s *next;时,认为next是一个指向不完全类型的指针,当处理到第四行};时,struct s成了一个完全类型,next也成了一个指向完全类型的指针。类似这样的结构体是很多种数据结构的基本组成单元,如链表、二叉树等。下图示意了由struct s结构体组成的链表。

 

可以想像得到,如果把指针和数组、函数、结构体层层组合起来可以构成非常复杂的类型,下面看几个复杂的声明。

 

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);

这个声明来自signal(2)。sighandler_t是一个函数指针,它所指向的函数带一个参数,返回值为void,signal是一个函数,它带两个参数,一个int参数,一个sighandler_t参数,返回值也是sighandler_t参数。如果把这两行合成一行写,就是:

 

void (*signal(int signum, void(*handler)(int)))(int);

在分析复杂声明时,要借助typedef把复杂声明分解成几种基本形式:

 

T *p; /*p是指向T类型的指针。*/
 
T a[]; /*a是由T类型的元素组成的数组,但有一个例外,如果a是函数的形参,则相当于T *a; */
 
T1 f(T2, T3...);  /*f是一个函数,参数类型是T2、T3等等,返回值类型是T1。*/

 

我们分解一下这个复杂声明:

 

int (*(*fp)(void *))[10];

1、fp和*号括在一起,说明fp是一个指针,指向T1类型:

 

typedef int (*T1(void *))[10];
T1 *fp;


2、T1应该是一个函数类型,参数是void*,返回值是T2类型:

 

typedef int (*T2)[10];
typedef T2 T1(void *);
T1 *fp;

 

3、T2和*号括在一起,应该也是个指针,指向T3类型:

 

typedef int T3[10];
typedef T3 *T2;
typedef T2 T1(void *);
T1 *fp;

显然,T3是一个int数组,由10个元素组成。

如果我们的声明的结构体和联合体不在程序的其他部分使用,那么头文件可以声明不带该内容的标记。在程序的其他部分可以使用指向不完全结构或联合的指针,只要不去使用它的成员就不会出现任何问题。

频繁使用的不完全类型是未指定长度的外部数组。通常情况下,我们要使用某个数组的内容,不需要知道该数组的范围。


 

转载于:https://www.cnblogs.com/new0801/p/6177080.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值