Firefox 3

常用链接

随笔分类

文章分类

博客链接

站点链接

最新评论

阅读排行榜

评论排行榜

[标题] char *(*(**(*(*(*x[5])(int,float))[][12])(double))(short,long))[][173] ?!

今天又捧起久违的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 char ArrayOfChar[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)等于多少?

0
0
(请您对文章做出评价)
« 上一篇:[快讯] Visual Studio 2005和SQL Server 2005来了!
» 下一篇:Windows Live Beta ONLINE!
posted on 2005-11-06 12:39 neoragex2002 阅读(4582) 评论(22)  编辑 收藏 网摘 所属分类: Win32环境

FeedBack:
#1楼 2005-11-06 14:00 Ninputer[未注册用户]
我有一点弄不明白,为什么我们需要能看懂和写出char *(*(**(*(*(*x[5])(int,float))[][12])(double))(short,long))[][173];这样的代码呢……
  回复  引用    
#2楼[楼主] 2005-11-06 14:15 neoragex2002      
为了理解编译器。在抛弃指针之前最好还是先知道它是怎么一回事,否则丢了容易,捡回来就难了。
  回复  引用  查看    
#3楼 2005-11-06 14:20 无常      
有意义吗?
  回复  引用  查看    
#4楼[楼主] 2005-11-06 14:23 neoragex2002      
看以什么标准对什么人做什么事了。
  回复  引用  查看    
#5楼[楼主] 2005-11-06 15:34 neoragex2002      
挑战答案:p=0x1238,sizeof(x)=20
  回复  引用  查看    
#6楼 2005-11-06 20:46 吕震宇      
好文!
  回复  引用  查看    
#7楼 2005-11-07 10:35 DDD[未注册用户]
跟天书一样
  回复  引用    
#8楼 2005-11-07 11:34 oop80      
感觉实用意义不大,但是学习学习,搞搞头脑还是有点意思的
  回复  引用  查看    
#9楼[楼主] 2005-11-07 12:59 neoragex2002      
对于应用开发而言,既然可以避开指针的泥潭,自然不甚实用,这个不用再谈了,没啥意思XD 请自行忽略。

说点有趣的,其实上文这点dd都是当初我在做完了一个蹩脚的qbasic解释器后,继续做一个同样蹩脚的C解释/编译器时总结的,由于是一个人做着玩,所以没有用表驱动,用的递归下降。不知有没有搞过C解释或编译的兄弟路过,我很想听听你们的意见,尤其是指针分析和类型验证方面的,随便聊聊。

  回复  引用  查看    
#10楼 2005-11-07 23:02 billjoy[未注册用户]
看的我冒汗~~~~
有点意思!

  回复  引用    
#11楼 2005-11-20 23:04 SEMAN      
这是对最基本语法的掌握与扩充,不过这么复杂的表达式还是不要挑战为好 呵呵
  回复  引用  查看    
#12楼 2006-11-15 17:41 ab[未注册用户]
写代码的第一要则就是易懂
  回复  引用    
#13楼[楼主] 2006-11-15 20:00 neoragex2002      
@ab
Oh, I can't agree more about it. As everybody knows, that is an essential requirement for software blue collars.

  回复  引用  查看    
char (*q)[][12];

对这个指针该如何赋值呢?

  回复  引用    
#15楼[楼主] 2006-11-27 14:56 neoragex2002      
char (*q)[][2]

typedef char A;
typedef A B[2];
typedef B C[];
typedef C *D;

D p;

以上的p和q是类型等价的。

实际上,你会发现变量定义C xx是错误的,而C yy={1,2,3,4}却是对的。这主要是因为[]这种省写方式只是一种词法形式而已,在编译时其文法必须具有合理的解释,即其sizeof在编译时必须是确定的,此处xx是不确定的,而yy是确定的,因此xx不合法。

因此,我们可以得出结论,类似char (*q)[][2]这种指针定义仅仅在词法角度是合法的,但是从语义角度解释,它却是不合理的(尽管编译器不报错)。什么叫做“指向一个第一维具有12个char、而第二维却不确定的数组的指针”?如果有个操作是p++的话,那么你认为p的地址增量应该是多少呢?唬,没人知道。而另一方面,实际上,所有采用[]省写的二维数组在语义分析阶段其第二维是已经确定了的,所以,怎么都无法跟这个指针的“不确定”语义匹配。因此,除了NULL外,无法对这个q进行赋值。

  回复  引用  查看    
在32位环境下,假设void* p=(void *)(x+1),x=0x1234;则p的16进制值为多少?sizeof(x)等于多少?

这个题怎么解能说具体点吗?

  回复  引用    
#17楼[楼主] 2007-03-17 23:03 neoragex2002      
@迷惑的石头
呵呵,你还真做啊。按我提到的结合律分解一下这个式子,其实非常简单,一大串都故意唬人的:
char *(*(**(*(*(*x[5])(int,float))[][12])(double))(short,long))[][173];
就是x->[5]->*->....(后面的根本就是迷惑信息),即x是一个拥有5个元素的数组,数组中每个元素都是一个指针,其指针类型我们根本没有必要关心。这样答案便很清楚了,x指向0x1234,则x+1指向0x1238,因为32位指针是4个字节,而sizeof(x)=4*5=20。

  回复  引用  查看    
#18楼 2007-05-23 23:57 tk446[未注册用户]
早知买"C程序设计"的第二版,第三版没了c++部分,价钱还是一样的

内容也没怎么变吧~!

  回复  引用    
楼主写的很好,解释的也很清楚,十分感谢
  回复  引用    
#20楼 2008-12-10 19:48 喂喂[未注册用户]
收益匪浅~
  回复  引用    
#21楼 2008-12-10 20:03 求助[未注册用户]
char (*(*x())[])()怎么解释呢??
  回复  引用