C 语言的变量声明解析规则:顺时针规则(Clockwise/Spiral Rule)

最近在尝试入门 Go 语言,发现 Go 在声明变量类型的时候,采用了将类型置于变量之后的方式:

1
2
3
x int // x: int
p *int // p: pointer to int
a [3]int // a: array[3] of int

虽然看起来和 C 的结构差不多,但顺序完全不同:读法是从右往左阅读。详见:Go's Declaration Syntax

那 C 的读法就是从左往右阅读的吗?不是的。它遵循的是「顺时针/螺旋规则(Clockwise/Spiral Rule)」(David Anderson, 1994)。

先啰嗦几句

学校里涉及 C 的课程有介绍 C 的,最近又在选修 C++,但是关于变量声明的各种写法,老师貌似一直都没有一个很明确的规则去帮助学生记忆……导致我尝尝是靠「感觉」和一些类比来写 C 的变量声明。但是当碰到相当复杂的结构时,就常常被水淹没,不知所措:

1
int (*(*fp)(int (*)(int, int), int))(int, int)

此处的 fp 是什么呢?

它前面有个 *,是个「指针」……后面又有个参数传递?所以是「指向函数的指针」。它返回的是前面的 *,就是「指向一个返回一个指针的函数」,那这整个是……啊什么鬼看不懂!

直到今天,我认识到 Go 的这种变量声明方式解决了 C 的痛点的同时,也看到了 C 的解析规则(顺时针规则),这才学会游泳,如鱼得水。

顺时针/螺旋规则(Clockwise/Spiral Rule)

学会了这个规则,我们就能在脑海中自己解析 C 的任何声明了!

步骤很简单:

  1. 从未知元素开始,沿着顺时针/螺旋方向移动,当碰到下一个元素时,将之替换为以下对应的英语语句:
    • [X] => Array X size of...
    • [] => Array undefined size of...
    • (type1, type2) => function passing type1 and type2 returning...
    • * => pointer(s) to
  2. 重复上一步直到所有元素都被记录到了。
  3. 记住先解决掉括号里的所有东西。

例子 #1:简单声明(Simple declaration)

1
2
3
4
5
6
7
     +-------+
| +-+ |
| ^ | |
char *str[10];
^ ^ | |
| +---+ |
+-----------+

我们先问自己一个问题:str 是什么?

str is an...

我们从 str 开始,沿顺时针方向移动,碰到的第一个字符是 [,这表示我们有一个 array,所以:

str is an array 10 of...

继续移动,下一个是 *,所以:

str is an array 10 of pointers to...

继续移动,下一个是 ;,不管它。

继续移动,下一个是 char,所以:

str is an array 10 of pointers to char.

所有的元素都被访问到了,我们结束啦!

例子 #2:函数的指针声明(Pointer to Function declaration)

1
2
3
4
5
6
7
8
9
     +--------------------+
| +---+ |
| |+-+| |
| |^ || |
char *(*fp)( int, float *);
^ ^ ^ || |
| | +--+| |
| +-----+ |
+------------------------+

我们先问自己一个问题:fp 是什么?

fp is a...

我们从 fp 开始,沿顺时针方向移动,碰到的第一个字符是 ),这表示 fp 在括号里,不管它。

继续移动,下一个是 *,所以:

fp is a pointer to...

继续移动,下一个是 (,这表示我们有一个 function,所以:

fp is a pointer to a function passing an int and a pointer to float returning...

继续移动,下一个是 *,所以:

fp is a pointer to a function passing an int and a pointer to float returning a pointer to...

继续移动,下一个是 ;,不管它。

继续移动,下一个是 char,所以:

fp is a pointer to a function passing an int and a pointer to float returning a pointer to a char.

翻译成中文:

fp 是个指针,指向一个函数,该函数:

  • 参数:
    • 一个整数
    • 一个指针,指向一个浮点数
  • 返回:
    • 一个指针,指向一个字符

注意这里有的括号被访问到,有的没有。规则就是先解决掉括号里的所有东西。所以第一步我们没有越过括号,去访问外面的元素,而是绕了一圈,又回到了括号里的 *

例子 #3:终极(The "Ultimate")

1
2
3
4
5
6
7
8
9
      +-----------------------------+
| +---+ |
| +---+ |+-+| |
| ^ | |^ || |
void (*signal(int, void (*fp)(int)))(int);
^ ^ | ^ ^ || |
| +------+ | +--+| |
| +--------+ |
+----------------------------------+

我们先问自己一个问题:signal 是什么?

signal is a...

我们从 signal 开始,沿顺时针方向移动,碰到的第一个字符是 (,这表示我们有一个 function,所以:

signal is a function passing an int and a...

呃……我们碰到了第二个复杂的参数 fp,把它当成一个支线。我们用同样的解析规则。所以……什么是 fp 呢?

(支线)我们从 fp 开始,沿顺时针方向移动,碰到的第一个字符是 ),不管它。

(支线)继续移动,下一个是 *,所以:

fp is a pointer to...

(支线)继续移动,下一个是 (,所以:

fp is a pointer to a function passing int returning...

(支线)继续移动,下一个是 void,所以:

fp is a pointer to a function passing int returning nothing (void).

支线任务完成了。我们回到 signal,现在它是:

signal is a function passing an int and a pointer to a function passing an int returning nothing (void) returning...

继续移动,下一个是 *,所以:

signal is a function passing an int and a pointer to a function passing an int returning nothing (void) returning a pointer to...

继续移动,下一个是 (,所以:

signal is a function passing an int and a pointer to a function passing an int returning nothing (void) returning a pointer to a function passing an int returning...

继续移动,下一个是 void,所以:

signal is a function passing an int and a pointer to a function passing an int returning nothing (void) returning a pointer to a function passing an int returning nothing (void).

翻译成中文:

signal 是个函数,该函数:

  • 参数:
    • 一个整数
    • 一个指针:指向一个函数,该函数:
      • 参数:一个整数
      • 返回:void
  • 返回:
    • 一个函数,该函数:
      • 参数:一个整数
      • 返回:void

更多例子

同样的规则适用于 constvolatile,比如:

1
const char *chptr;

chptr is a pointer to a char constant.

此处注意 const 位于首位时,与其后的元素交换后的结果是等价的。所以 const charchar const 是一样的,而前者更符合代码习惯,后者更符合语言习惯。所以上面那句应该表达为

chptr is a pointer to a constant char.

1
char * const chptr;

chptr is a constant pointer to a char.

1
volatile char * const chptr;

chptr is a constant pointer to a char volatile.

解决开篇的问题

开篇那个复杂的例子是不是迎刃而解?

1
int (*(*fp)(int (*)(int, int), int))(int, int)

fp is a pointer to a function passing (a pointer to a function passing two ints returning an int) and (an int) returning a pointer to a function passing two ints returning an int.

翻译成中文:

fp 是个指针,它指向一个函数,该函数:

  • 参数:
    • 一个指针,指向一个函数,该函数:
      • 参数:两个整数
      • 返回:一个整数
    • 一个整数
  • 返回:
    • 一个指针,指向一个函数,该函数:
      • 参数:两个整数
      • 返回:一个整数

更多练习

K&R II Page 122

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int *f();
/* f: function returning pointer to int */

int (*pf)();
/* pf: pointer to function returning int */

char **argv;
/* argv: pointer to pointer to char */

int (*daytab)[13];
/* daytab: pointer to array[13] of int */

int *daytab[13];
/* daytab: array[13] of pointer to int */

void *comp();
/* comp: function returning pointer to void */

void (*comp)();
/* comp: pointer to function returning void */

char (*(*x())[])();
/* x: function returning pointer to array[] of pointer to function returning char */

char (*(*x[3])())[5];
/* x: array[3] of pointer to function returning pointer to array[S] of char */