9. 不完全类型和复杂声明
在第 1 节 “复合类型与结构体”讲过算术类型、标量类型的概念,现在又学习了几种类型,我们完整地总结一下C语言的类型。下图出自[Standard C]。
图 23.5. C语言类型总结
C语言的类型分为函数类型、对象类型和不完全类型三大类。对象类型又分为标量类型和非标量类型。指针类型属于标量类型,因此也可以做逻辑与、或、非运算的操作数和if
、for
、while
的控制表达式,NULL
指针表示假,非NULL
指针表示真。不完全类型是暂时没有完全定义好的类型,编译器不知道这种类型该占几个字节的存储空间,例如:
- struct s;
- union u;
- char str[];
具有不完全类型的变量可以通过多次声明组合成一个完全类型,比如数组str
声明两次:
- char str[];
- char str[10];
当编译器碰到第一个声明时,认为str
是一个不完全类型,碰到第二个声明时str
就组合成完全类型了,如果编译器处理到程序文件的末尾仍然无法把str
组合成一个完全类型,就会报错。读者可能会想,这个语法有什么用呢?为何不在第一次声明时就把str
声明成完全类型?有些情况下这么做有一定的理由,比如第一个声明是写在头文件里的,第二个声明写在.c
文件里,这样如果要改数组长度,只改.c
文件就行了,头文件可以不用改。
不完全的结构体类型有重要作用:
- struct s {
- struct t *pt;
- };
- struct t {
- struct s *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 {
- struct t ot;
- };
- struct t {
- struct s os;
- };
编译器看到struct s { struct t ot; };
时,认为struct t
是一个不完全类型,无法定义成员ot
,因为不知道它该占几个字节。所以结构体中可以递归地定义指针成员,但不能递归地定义变量成员,你可以设想一下,假如允许递归地定义变量成员,struct s
中有一个struct t
,struct t
中又有一个struct s
,struct s
又中有一个struct t
,这就成了一个无穷递归的定义。
以上是两个结构体构成的递归定义,一个结构体也可以递归定义:
- struct s {
- char data[6];
- struct s* next;
- };
当编译器处理到第一行struct s {
时,认为struct s
是一个不完全类型,当处理到第三行struct s *next;
时,认为next
是一个指向不完全类型的指针,当处理到第四行};
时,struct s
成了一个完全类型,next
也成了一个指向完全类型的指针。类似这样的结构体是很多种数据结构的基本组成单元,如链表、二叉树等,我们将在后面详细介绍。下图示意了由几个struct s
结构体组成的链表,这些结构体称为链表的节点(Node)。
图 23.6. 链表
head
指针是链表的头指针,指向第一个节点,每个节点的next
指针域指向下一个节点,最后一个节点的next
指针域为NULL
,在图中用0表示。
可以想像得到,如果把指针和数组、函数、结构体层层组合起来可以构成非常复杂的类型,下面看几个复杂的声明。
- 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个元素组成。分解完毕。