AImager

C语言中的复杂声明

#c

(*(void (*)())0)(),这条怪异的C语句来自于经典的C语言书籍——《C陷阱与缺陷》,那它的作用呢?不卖关子,这句话就是用来执行内存地址为0的函数,但如何理解这句话呢?先只看中间的void (*)()部分,好像有点眼熟,没错,就是中国的C语言教育很喜欢比较的两个对象int (*)[]int* []中的前者——指向某个整形数组的指针类型,类比到void (*)(),即指向某个void函数的指针类型,然后向外面扩展,因为优先级的关系会先结合左边的*,从而表示对void (*)()的解引用,得到void函数类型,而void函数类型实则就是我们抽象意义上说的函数名指针(这只是语法层次的表述),当这种类型作为一种强制转换加持到右边的0常数上的时候,则变成了一个地址为0的「函数名」,最后与()作用,驱使0地址的函数执行。

对于上面这条C语句,其复杂程度本质上还是体现在C语言的声明上面,《C程序设计语言》甚至专门分了一节来讲解C语言的复杂声明,为了搞清楚这些申明,我们再看两个例子。

char (*(*x())[4])(),还是先抽出直接关系变量名的*x(),根据结合方向,判断x是个返回指针的函数,把(*x())作为整体放到*(*x())[4]中,则x函数返回的指针指向一个4元素数组,数组的每个元素都是一个指针,最后合并整个声明,数组元素指针即指向一个返回值为char的函数。

char (*(*x[3])())[5],抽出直接关系变量名的*x[3],x表示一个3元素数组,数组每个元素是个指针,继续抽出*(*x[3])(),数组每个元素指针均各向一个函数,这些函数返回的均是指针,合并整个声明,即函数返回指针指向一个char类型的5元素数组。

细心的各位应该已经发现了两段分析的共性,即均是先抽出与变量名关系最紧的部分,分析完后,再把这部分作为整体放入原声明式中继续分析。简而言之,就是一个从最底层向外不断分析的递推过程,既然可以递推,自然是可以通过抽象递推元素来进行递推编码的,事实上,《C程序设计语言》已经给出了抽象方法,即将所有的声明式最终抽象成dcl和direct-dcl两种元素组成的式子,这样在每次分析的时候,均只需要关注局部递推信息即可。以下为《C程序设计语言》里dcl和direct-dcl的定义

dcl:前面带可选个*的direct-dcl
direct-dcl:变量名、(dcl)、direct-dcl()、direct-dcl[可选的长度]

关于dcl和direct-dcl的部分只说这些,事实上,dcl和direct-dcl是在了解C语言复杂声明后的一种理论实现,所以关键还是在于如何去分析理解一条复杂声明,除了上面说的分析方法,深入理解函数、指针、数组也是必不可少的。

强制转换

C语言是静态类型语言,所以在不同类型变量之间交换数据的时候,就需要进行强制转换,如何分析强制转换的类型含义呢?一句话,从左往右最后一个指针的含义即表示强转类型。什么,一个*都没有?那显然不是复杂声明,因为根据dcl和direct_dcl的规则,声明没法递推下去,那char a[]()[]()可不可以呢?显然不行,语法不支持。最后给个实例

int** test = malloc(sizeof(int(*))*2);
test[0] = malloc(sizeof(int)*2);
test[1] = malloc(sizeof(int)*2);

int (*(*changeTest)[2])[2] = (int (*(*)[2])[2])test;    // 将malloc的类型转换为数组类型,方便编辑器分析

其实,无论从可读性还是安全性上讲,复杂声明都不算是个好写法,但凡事都要考虑历史进程,在80年代高级语言的封装层次远不如今天这么高的时候,能用一句话完成指定内存级别代码段的运行,不得不说还是非常有效的,甚至说是酷炫的。但酷炫归酷炫,偶尔用来脑力激荡下还行,团队协作中大家就不要这么写了,当然,如果你是为了参加IOCCC,那完全可以忽略这条建议。