最近在尝试入门 Go 语言,发现 Go 在声明变量类型的时候,采用了将类型置于变量之后的方式:
1 | x int // x: 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 的任何声明了!
步骤很简单:
- 从未知元素开始,沿着顺时针/螺旋方向移动,当碰到下一个元素时,将之替换为以下对应的英语语句:
[X]
=> ArrayX
size of...[]
=> Arrayundefined
size of...(type1, type2)
=> function passingtype1
andtype2
returning...*
=> pointer(s) to
- 重复上一步直到所有元素都被记录到了。
- 记住先解决掉括号里的所有东西。
例子 #1:简单声明(Simple declaration)
1 | +-------+ |
我们先问自己一个问题: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 | +--------------------+ |
我们先问自己一个问题: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 | +-----------------------------+ |
我们先问自己一个问题: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
更多例子
同样的规则适用于 const
、volatile
,比如:
1 | const char *chptr; |
chptr
is a pointer to a char constant.
此处注意 const
位于首位时,与其后的元素交换后的结果是等价的。所以 const char
和 char 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 | int *f(); |