C语言本身提供了一种不甚明确的变量声明方式——基于使用的声明,如int *a,本质上是声明了*a的类型为int,所以得到了a的类型为指向int的指针。对于简单类型,这样声明并不会对代码产生多大的阅读障碍,而对于复杂的声明,比如标准库的signal函数签名,void (*signal( int sig, void (*handler) (int))) (int),这是什么?一眼看不出来吧,这是一个函数,接受两个参数,一个int,一个函数指针,而这个函数指针指向的函数接受一个int并返回void;返回一个函数指针,这个函数指针指向的函数接受一个int并返回void。尽管很多人都吵着说工程中谁写出来这样的代码就炒他鱿鱼,但人家标准库的确是写出了这样的代码的,你怎么办?
解析这样的复杂声明,我之前见过一种方法——右旋转法,方法是这样的:
- 从变量名开始,先右再左地,交替地一个一个向外看旁边的token,在纸上写下:“变量是”
- 若向右遇到左圆括号,在纸上写下:“函数,参数是”,并用同样的方法处理括号中每一个参数——在纸上写下:“返回”
- 若向右遇到方括号,在纸上写下:“数组,长度为{方括号的内容},元素类型为”
- 若向右遇到右圆括号,什么也不做
- 若向左遇到*,在纸上写下:“指针,指向”
- 若向左遇到任何类型,在纸上写下对应的类型名
我们用这种方法来处理下面的声明
void*(*(*fp1)(int))[10]
- 从fp1开始——fp1是
- 向右,遇到右括号,什么也不做
- 向左,遇到*——指针,指向
- 向右,遇到左圆括号——函数,参数是int,返回
- 向左,遇到*——指针,指向
- 向右,遇到左方括号——数组,长度为10,元素类型为
- 向左,遇到*——指针,指向
- 向右,已经到声明结尾,什么也不做
- 向左,遇到void——void
结果是:fp1是 指针,指向 函数,参数是int,返回指针,指向数组,长度为10,元素类型为 指针,指向 void
这种方法对于人来讲是比较合适的,因为他比较符合人脑的处理方式,但是也有一点缺点,如果函数的形参也写了名字,不是很熟练的小白,就不容易找到正确的起始位置,造成处理的混乱。
对于机器处理,这种从中间到两边的方法就不是很合适了,因为机器并不能直接在一个token序列中直接找到处理的起始位置,他只能从左到右进行扫描,我昨晚灵机一动想到一个算法,今天进行了试验,效果良好,没有对比一些比如cdecl.org那样的开源实现,我这个算法只是一个demo,并不完整支持C声明的处理,地址在这里。
算法从左到右扫描,本质上是递归下降,基本过程是这样的,为了方便说明,递归函数名为parse:
- parse开始
- if遇到类型,保存进变量a,递归parse,输出a
- elif遇到*,递归parse,输出"pointer to"
- elif遇到左圆括号,递归parse,并检查括号匹配
- elif遇到标识符,输出"{标识符} is"
- 控制流继续
- if遇到左方括号,输出"array with length {长度} of",并检查括号匹配
- elif遇到左圆括号,输出"function accepting",循环地递归parse,吃掉后面的逗号,直到遇到右圆括号,输出"returning"
- 返回
递归的意义是什么?每一个parse函数的意义都是:“我处理的这段东西的类型是——”,破折号后面的东西右这一层函数退出后上一层函数来补完,所以回去看上面的算法,遇到一个类型的时候,我就明白我下面一层处理的这段东西的类型是int,所以我递归调用parse,并输出int。
再从循环不变式的角度看parse的递归,parse的出口只有一个,那就是遇到的第一个if,不管是什么样的函数声明,最后都是以一个变量结尾的,而这里也是唯一一个把话说全了的分支——其他的分支都是输出类似于“xxx是”,“xxx返回”这样的没说完的话的,所以parse保证上一层的话都没有说完——从不是第一个if的分支退出,由这一层把话补全,这算某种意义上的循环不变式吧。
最后我把上面的声明拆成不同层数来表现一下parse的过程
void * [10] ( ) * (int) ( ) * fp1