C 的类型语法
C 的类型声明语法以难读著称,尤其是函数指针、数组指针这类声明,经常让人感到绕晕。问题并不在于类型本身有多神秘,而在于 C 把“类型”和“变量名的使用形式”混在一起书写。
本章的目标不是让你死记硬背规则,而是给你一套稳定的读法:先把类型想象成一张图,再把这张图翻译成 C 的声明形式。
读完本章后,你应该能拆解 void (*x)(int)、void (*signal(int, void (*)(int)))(int) 这类声明,并能判断一个声明到底表示“指针”“数组”“函数”,还是这些类型的组合。
用图理解类型
C 能表达的类型其实并不复杂,主要就是基本类型、指针、数组和函数。复杂声明难读的原因,是这些构造可以任意互相嵌套。
因此,我们先把语法形式放到一边,只看类型的结构关系,这样理解起来会容易得多。
下图表示“指向 int 的指针的指针”。从变量出发,沿着箭头依次读:先是一个指针,该指针指向另一个指针,后者再指向 int。
int ** 的类型图也就是说,如果变量 x 具有上面的类型,那么 x 本身是指针;*x 仍然是指针;**x 才是 int。
数组也可以用同样的方法表示。下面的图表示“长度为 20 的数组,数组元素是指向 int 的指针”。
int *x[20] 的类型图函数类型也一样可以画出来。下面的图表示一个函数:它接收两个参数,第一个是 int,第二个是 int *,返回值是 void *。
再看一个更复杂的类型:指向函数的指针,而这个函数又返回另一个函数指针。
文字描述会显得很绕,但图里只是几段箭头的连接。
真正的编译器内部也会采用类似的结构来表示类型:一个类型节点指向另一个类型节点。
指针类型指向它所指向的类型;数组类型保存元素类型和长度;函数类型保存参数类型列表和返回值类型。
因此,上面的图不是比喻,而是非常接近编译器内部数据结构的表示。
一种便于阅读的类型记法
为了说明方便,本章使用一种比 C 更直观的记法来描述类型:
int、char、void表示基本类型;* T表示“指向T的指针”;[N] T表示“长度为N、元素类型为T的数组”;func(A, B) R表示“接收A、B两个参数并返回R的函数”。
例如 int ** 可以写成 * * int;int *x[20] 的类型可以写成 [20] * int;接收 int 和 int * 并返回 void * 的函数可以写成 func(int, * int) * void。
这种记法不是 C 语法,而是本章为了说明类型结构引入的中间表示。它的优点是可以从左到右阅读,不需要在括号、星号、数组后缀和函数参数列表之间来回跳转。
从类型设计的角度看,这种记法也更接近 Go 语言等现代语言的写法:类型构造按照实际结构顺序排列,读者先看到“这是数组、指针还是函数”,再继续追踪它指向、包含或返回的类型。
C 类型声明的读法
C 声明通常可以拆成四个部分:
- 基础类型,例如
int、unsigned int、void; - 基础类型后面的星号,例如
*、**; - 标识符,或用括号包起来的嵌套声明;
- 标识符后面的数组括号
[]或函数参数列表()。
例如 unsigned int *x() 可以拆成:基础类型 unsigned int,星号 *,标识符 x,以及函数参数列表 ()。这表示 x 是一个函数,返回值是 unsigned int *。
没有嵌套括号的声明
如果星号后面直接跟变量名,读起来比较直接。下面是几个基本例子。
| C 声明 | 含义记法 | 中文说明 |
|---|---|---|
int x | int | x 是 int |
int *x | * int | x 是指向 int 的指针 |
int **x | * * int | x 是指向 int 指针的指针 |
函数声明中,变量名后面的 () 表示“这是函数”。返回值类型由基础类型和星号部分决定。
| C 声明 | 含义记法 | 中文说明 |
|---|---|---|
int x() | func() int | x 是返回 int 的函数 |
int *x() | func() * int | x 是返回 int 指针的函数 |
int **x(int) | func(int) * * int | x 接收 int,返回 int 指针的指针 |
数组声明中,变量名后面的 [N] 表示“这是数组”。数组的元素类型由其余部分决定。
| C 声明 | 含义记法 | 中文说明 |
|---|---|---|
int x[5] | [5] int | x 是长度为 5 的 int 数组 |
int *x[5] | [5] * int | x 是长度为 5 的数组,元素是 int 指针 |
int **x[4][5] | [4] [5] * * int | x 是二维数组,元素是 int 指针的指针 |
带嵌套括号的声明
声明里出现 (*x) 这类括号时,要把括号内外分开读。括号的作用是改变结合顺序:如果没有括号,[] 和 () 会优先和变量名结合;加上括号后,星号可以先和变量名结合。
以 int (*x)() 为例。先把括号里的 *x 看成一个占位类型 * ___。括号外的 int y() 表示 func() int。把外层类型填入占位处,得到 * func() int。因此 x 是一个指针,指向一个“返回 int 的函数”。
再看 void (*x[20])(int)。括号里的 *x[20] 表示 [20] * ___;括号外的 void y(int) 表示 func(int) void。组合后得到 [20] * func(int) void。也就是说,x 是长度为 20 的数组,数组元素是函数指针,所指函数接收 int 并返回 void。
下面是 Unix 中著名的 signal 函数声明:
void (*signal(int, void (*)(int)))(int);这条声明可以按“先分开、再填回”的方式读。先把最外层括号 (*signal(int, void (*)(int))) 暂时当作一个普通标识符 y,外层就变成 void y(int),也就是 func(int) void。
再看括号内部:*signal(int, void (*)(int))。其中 signal(...) 表示 signal 是函数;前面的 * 表示这个函数的返回值要填进一个“指针”结构中。因此内层的大致形状是 func(参数1, 参数2) * ___。
- 第一个参数是
int。 - 第二个参数是
void (*)(int),也就是“指向函数的指针”;该函数接收int,返回void。用本章记法写作* func(int) void。
把两个参数填入内层,再把外层返回类型填回占位符,最终得到 func(int, * func(int) void) * func(int) void。换句话说,signal 是一个函数:它接收一个 int 和一个函数指针,并返回另一个函数指针;这两个函数指针所指向的函数都接收 int 并返回 void。
这条声明可以读作:signal 是一个函数,它接收两个参数:第一个参数是 int,第二个参数是“指向函数的指针”,该函数接收 int 并返回 void。signal 的返回值也是一个函数指针,所指函数同样接收 int 并返回 void。
专栏:C 声明语法的设计意图
C 的声明语法有一个基本思路:声明时的写法尽量接近使用时的写法。例如 int *x[20] 可以理解为:如果写 *x[20],得到的是 int。因此 x 本身必须是“数组,数组元素是指针”。这个设计有内在逻辑,但可读性并不好。复杂声明中,人脑需要反复在标识符、括号、星号和参数列表之间跳转,所以实际写代码时,应尽量用 typedef 或分解声明来降低复杂度。
例如 int *(*x[20])() 可以理解成这样一个问题:怎样给 x 定义类型,才能让表达式 *(*x[20])() 的结果是 int?顺着使用形式倒推,x 首先必须是数组,数组元素必须能被解引用,解引用后还要能作为函数调用,而函数返回值还要再解引用一次。这个思路解释了 C 声明为什么“有逻辑但不好读”。
练习题
下面的表把若干 C 声明转换成本章使用的类型记法。读的时候建议先找到变量名,再按“后缀优先、括号改变结合顺序”的规则展开。
| C 声明 | 含义记法 |
|---|---|
int x | int |
int *x | * int |
int x[] | [] int |
int x() | func() int |
int **x | * * int |
int (*x)[] | * [] int |
int (*x)() | * func() int |
int *x[] | [] * int |
int x[][] | [] [] int |
int *x() | func() * int |
int ***x | * * * int |
int (**x)[] | * * [] int |
int (**x)() | * * func() int |
int *(*x)[] | * [] * int |
int (*x)[][] | * [] [] int |
int *(*x)() | * func() * int |
int **x[] | [] * * int |
int (*x[])[] | [] * [] int |
int (*x[])() | [] * func() int |
int *x[][] | [] [] * int |
int x[][][] | [] [] [] int |
int **x() | func() * * int |
int (*x())[] | func() * [] int |
int (*x())() | func() * func() int |