[标题] 一劳永逸:关于C/C++中指针、数组与函数复合定义形式的直观解释
今天又捧起久违的K&R C拜读了一遍。其实有点东西在6年前就想写,借着今天这个机会,终于把它写出来了。
初看一眼标题中的变量定义感觉是不是很抓狂?:)一直以来,C语言中关于指针、数据和函数的复合定义都是一个难点,其实,理解它也是有规律可循的。然而,即便是国内在讲解指针方面久负盛名的“谭本”也没有将这一规律性说清楚,K&R C虽然提到了一点,却始终没有捅破这层窗户纸,也许是K&R觉得以“直观方式”解释太阳春白雪了点吧:)在Blog上面说说这种不值一提的dd倒正合适。
其实,理解C语言中复合定义的关键在于对变量声明语句中各修饰符结合律的把握,我们可以将它们的结合规律简单归纳如下:
(1) 函数修饰符 ( ) 从左至右
(2) 数组修饰符 [ ] 从左至右
(3) 指针修饰符 * 从右至左
其中,(1)与(2)的修饰优先级是相同的,而(3)比前两者的优先级都低,而且是写在左边的。下面我们给出3个直观的例子来说明如何借助结合律来理解复合变量声明,为了简单点,函数修饰符一律使用无形参的签名形式。
示例1. char (*(*x[3])())[5]
这是什么意思?别急,跟着走一遭咱就知道是什么了。根据结合律,我们可以依次写出与x结合的修饰符:
x -1-> [3] -2-> * -3-> () -4-> * -5-> [5] -6-> char
然后我们再来从左至右地对上述过程进行解释:
1说明:x是一个一维数组,数组中元素个数为3
2说明:上述数组中每一个元素都是一个指针
3说明:这些指针都是函数的指针,该函数的签名为( )
4说明:上面的函数所返回的值是一个指针
5说明:上面的指针所指向的是一个一维数组,其元素个数为5
6说明:上面的数组中的每一个元素均是一个字符
不知大家在上面的规范化步骤描述中看出端倪来了没有?:)这个声明的含义是:x是一个由3个指向函数A的指针所组成的一维数组,函数A返回指向一个元素个数为5的字符数组的指针。其实,以结合律来解析复合声明的方式是一种“由近及远”的方式:首先尝试着去说清楚离变量“近”的修饰符的含义,然后再对“远处”的修饰符进行依次说明,从抽象到具体,从顶到底,层层细化。
实际上,我比较反感这种一步到位的复合方式,它不仅把变量定义和类型声明混为一谈,而且也不能直观地体现出类型的含义,更糟糕的是,这不符合典型的“积木化”的程序思维,我更倾向于采用typedef,以一种“由远及近”的方式来逐步定义变量的形态,即先定义若干基本类型,然后再在其基础上将其扩充成复杂类型,最后利用复杂类型定义变量。例如,上述的例子,如果要我来定义,我觉得如此定义比较恰当:
typedef charArrayOfChar[5];
typedef ArrayOfChar* PointerOfArrayOfChar;
typedef PointerOfArrayOfChar (*PointerOfFunc)()
typedef PointerOfFunc ArrayOfPointerOfFunc[3]
ArrayOfPointerOfFunc pfa;
这种“堆积木”的方式实际上和那个复合声明是等价的,其看似繁冗,但对于程序员而言却很直观,所以平心而论,我比较推荐这种积木化声明方式,而不推荐以复合声明直接一步到位。
示例2.char (**x[3])()[5]
根据结合律,将上述声明改写如下:
x -1-> [3] -2-> * -3-> * -4-> () -5-> [5] -6-> char
1说明:x是一个数组,这个数组包括3个元素
2说明:每个元素均为一个指针
3说明:上面的指针又指向另一个指针
4说明:上面的第二个指针是一个函数的指针
5说明:上面的函数返回的是一个数组,这个数组包括5个元素?? (错误!)
从上述推导过程可以发现,当我们到达第5步时,其语义提到了“一个函数返回了一个数组”,这在C语言中实际上是错误的定义,即,( )与[ ]相邻是非法的,因此,编译器将拒绝接受这一关于x变量的声明。同样的,在推导过程中[ ]与( )相邻也是不合法的,什么叫做“一个数组,这个数组里面的每一个元素都是一个函数(而不是一个指针)”?在这种情况下,编译器也会100%报错。
示例3.char p[5][7]、char (*q)[7]、char *r[5] 和 char **s
不知p、q、r、s这四个变量类型是否兼容?根据结合律,有:
p -> [5] -> ([7] -> char) const
q -> * -> ([7] -> char)
r -> [5] -> { * -> char} const
s -> * -> { * -> char}
不难发现,无需经过类型强制转换即可将p赋值给q、将r赋给s,而其他的赋值方式均是错误的。为什么?首先,p和r是两个数组,不是指针,因此不能修改其值;其次,不妨让我们来对p与q(或者r与s)在其括号内的类型部分分别进行sizeof运算,可以发现,二者的结果是一样的,即:p、q(或者r、s)指针变量具备一致的增量寻址行为,所以二者才兼容。
看完了上述解释,想必最唬人的指针复合定义恐怕也难不倒你了。试试下面的挑战如何?
1. 解释一下x变量的含义:char *(*(**(*(*(*x[5])(int,float))[][12])(double))(short,long))[][173];
2. 在32位环境下,假设void* p=(void *)(x+1),x=0x1234;则p的16进制值为多少?sizeof(x)等于多少?