9. 不完全类型和复杂声明

第 1 节 “复合类型与结构体”讲过算术类型、标量类型的概念,现在又学习了几种类型,我们完整地总结一下C语言的类型。下图出自[Standard C]

图 23.5. C语言类型总结

C语言类型总结

C语言的类型分为函数类型、对象类型和不完全类型三大类。对象类型又分为标量类型和非标量类型。指针类型属于标量类型,因此也可以做逻辑与、或、非运算的操作数和ifforwhile的控制表达式,NULL指针表示假,非NULL指针表示真。不完全类型是暂时没有完全定义好的类型,编译器不知道这种类型该占几个字节的存储空间,例如:

  1. struct s;
  2. union u;
  3. char str[];

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

  1. char str[];
  2. char str[10];

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

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

  1. struct s {
  2. struct t *pt;
  3. };
  4.  
  5. struct t {
  6. struct s *ps;
  7. };

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

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

  1. struct s {
  2. struct t ot;
  3. };
  4.  
  5. struct t {
  6. struct s os;
  7. };

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

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

  1. struct s {
  2. char data[6];
  3. struct s* next;
  4. };

当编译器处理到第一行struct s {时,认为struct s是一个不完全类型,当处理到第三行struct s *next;时,认为next是一个指向不完全类型的指针,当处理到第四行};时,struct s成了一个完全类型,next也成了一个指向完全类型的指针。类似这样的结构体是很多种数据结构的基本组成单元,如链表、二叉树等,我们将在后面详细介绍。下图示意了由几个struct s结构体组成的链表,这些结构体称为链表的节点(Node)。

图 23.6. 链表

链表

head指针是链表的头指针,指向第一个节点,每个节点的next指针域指向下一个节点,最后一个节点的next指针域为NULL,在图中用0表示。

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

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

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

  1. 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是一个函数,参数类型是T2T3等等,返回值类型是T1

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

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

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

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

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

  1. typedef int (*T2)[10];
  2. typedef T2 T1(void *);
  3. T1 *fp;

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

  1. typedef int T3[10];
  2. typedef T3 *T2;
  3. typedef T2 T1(void *);
  4. T1 *fp;

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