转载自:http://soft.chinabyte.com/database/248/12193748.shtml
《C专家编程》第3章学习
C语言中的声明是一个比较令人头痛的问题,尤其一些复杂的声明看起来甚至会令人感到恐怖,如果您是一位初学者,您一定会对下面这几个式子感到畏惧。因为对于刚接触这种形式的人来说,这简直太复杂了,当然前提是假定您未掌握分析这方面的专业知识。
第一个声明: void(*signal(int sig, void (*func)(int)))(int);第二个声明:const char *const(*sinc(char *sincg(),int (*p)(int a,int *b)))(int **sing);其中第一个声明是某系统函数的声明,而第二个声明是我自己即兴创作的,但我敢保证它除了复杂一点之外没任何缺点。如果你对这两个声明感到异常头痛,并且急迫的想知道如何分析这样的声明来提高自己的c水平和熟练度。那么就请阅读下面的内容,让我将自己所学到的知识与您一起分享吧!
首先我们来看一个很简单的例子——复杂的东西都是由简单的构成,一旦将所有简单的都搞明白了,你才能,并且也可以轻易的弄懂复杂的问题,而那时候便只需要某一个转机就行了,那转机往往就是总结。
char next ;大家都很容易知道,这里声明了一个char 类型的变量next.这里的next是一个标识符,表明它的变量身份,所以我们可以这样想,一旦碰到标识符,如同next,就用这样一句话替代,(标识符)是………,比如前面的next,可以用next是……来替代,如同c语言中的宏定义一样,如果这样的说法让你感到有点晕,我们换一种方式,干脆这样定义一下,#define 标识符标识符是……
也就是说,一旦碰到标识符,你脑海中就立刻用(标识符)是………这样一句话来完全替代那个标识符。于是,刚才的那个声明,char next;我们用更规范的类似数学上的形式思维来考虑一下,首先我们分析标识符next,根据前面的说法,我们看到next之后,立刻用next是…。来替代,这样,我们就得到了整个声明的前一部分:“next是……。”这样一个句子,但是next究竟是什么呢,这时候我们需要看声明中的其他内容,比如上例中的char,这时候我们很自然的就弄清楚了next是(一个char类型的变量)。所以char表明的是一个类型变量。于是,经过这样一个思维过程,上面的声明就分析完成了。
大家先不要失去耐心,我用这么长的篇幅来叙述这样一个简单的例子并不是掉你胃口,相反,是为了让你更加容易理解下面的内容。使我的文字跳跃度不至于让你的思维感到突兀甚至短路——这不是我写这篇总结的目的。
接下来我们把这个声明稍微加长一点,使它变成char next();或者是char next[];这时候,根据我们所学的知识,依然能够很轻易的看出,前面一个是声明的返回char类型变量的函数,而后一个是声明一个char类 型变量的数组,正是这个时候,有一个问题请你注意,我们添加这个括号之后究竟使我们的思维改变了什么,为什么当这两个符号出现之后我们里就就明白了这是一 个函数或是一个数组,所以,在这里我想告诉你也许你从未意识到过的一个新知识点就是,圆括号和方括号是声明中的最高优先级[/B].这是一个非常重要的知 识点,当你发现标识符的右边紧挨着一个左圆括号(请一定注意到我这里写的是左圆括号[/B])或者是方括号的时候,你就不要有任何怀疑的告诉自己,标识符 是一个(返回…的函数)或者数组。让我们用这样的思维再来分析一下char next();这个声明,首先分析标识符next,得出next是…。,然后向右看它右边紧靠它的是不是圆括号或者方括号,这个例子中是一个圆括号,所以我们得出next是一个返回…。的函数,最后通过char类型符得出,next是一个返回char型变量的函数,至此,整个声明分析完成。
好了,现在让我们来总结声明分析中的前两个基本步骤,也是关键步骤。
首先,分析声明中的标识符,这里有一个问题,不知道大家发现没,我上面举的例子中都是只有一个标识符,如果声明中出现多个标识符怎么处理,比如说在next函数中增加几个参数,如char next(int a,int b);这 样的话,整个声明中有三个标识符,我们究竟首先选取分析哪个标识符呢。通过这个简单的例子,我们很容易观察出,首先选择的是最左边的标识符,当然,我们决 不能根据一个特例就得出普遍结论,在这里,我以一个已经掌握了这个知识点的人的身份告诉你,你的猜想是正确的,标识符从最左边的开始处理,c中确实是这样做的。那么现在,你也掌握了这个知识点,让我们继续向下讨论。当我们选定标识符后,就要观察紧靠它右边的声明器(也就是在声明中出现的各种各样的符号和变量,如(),*,const,[]等的官方说法),这里分两种情况:如果出现的是一个左圆括号,那么标识符就是一个返回…。的函数,如果出现的是一个方括号,则毫无疑问的说明,标识符是一个数组,至于是一个什么样的数组,则必须通过之后的分析才能够知道。
好了,现在又出现了一个新的问题,那就是如果标识符的紧邻的右边既不是左圆括号,也不是方括号怎么办?比如是一个右圆括号呢。这就需要我们的第三个比较重要的分析声明的步骤。那就是:看标识符左边的符号情况,分以下两种:A, 如果紧邻标识符左边的是一个左圆括号,则找寻到和它匹配的右圆括号,将整个括号内的声明当成一个整体分析。
B, 如果紧邻标识符左边的是*,或者const,或者volatile三者之一,则继续向左查找,直到声明器超出三者之外为止。也就是说要一直找到某个符号既不是*,const,也不是volatile为止。
C, 继B之后,如果符号是左圆括号,则回到A进行处理。
最后,剩下的符号可以一并阅读,因为那一定已经是非常容易理解的了。
在这里,我想为初学朋友解释一下const和volatile两个修饰符,const表示所修饰的变量是只读的,也就是一经赋值就不能再被修改。例如const char p; const char *p;此处需要大家注意的就是,const char *p;和char * const p;是不同的声明,前一个表明指针p所指向的内容是只读的,而后一个则表示指针本身是只读的,而它指向的内容则是可以改变的。我总结了一个规律供大家记忆方便,那就是如果 *和标识符是一个整体,没有被任何东西分开,则说明const修饰的是指针指向的内容,如上例中的const char *p;或者是char const *p;*和标识符(p)没有被const这个修饰符分开,则说明const修饰的是指针指向的内容,而一旦*和标识符被分开了,则说明const修饰的是指针本身,而指针指向的变量则是可变的。除非指向的变量本身也被const修饰了,如const char * const p;第二个修饰符volatile表明修饰的变量是他是可以被本程序和别的程序改变数据,象系统时间,不管这个程序是不是断点,sleep,别的程序都改变他的值。
好了,如果大家仔细阅读了上面的部分,一定会发现其实细心去分析之后,声明的理解也并不如何困难,现在我们先来用一个比较常规的声明来熟练一下上面的思维方式,最后我们以解决本文开始提到的两个声明作为结尾。
char (*p)(); //首先看最左边的标识符p,(表明p是……)他的右边既不是左圆括号也不是方括号,于是看左边,发现是*,根据上面的原则(如果紧邻标识符左边的是*,或者const,或者volatile三者之一,则继续向左查找,直到声明器超出三者之外为止。也就是说要一直找到某个符号既不是*,const,也不是volatile为止。)我们继续向左查找,发现下一个符号是(, 于是根据原则(继B之后,如果符号是左圆括号,则回到A进行处理。)我们找寻匹配这个左圆括号的右圆括号,那么,我们就可以把这个括号里的东西作为一个整体来处理,该例子中括号内的内容为*p,所以我们得出p是 一个指针,既然是一个指针,就肯定指向某一样东西,而这个东西,我们必须继续探究才能把它给挖出来,现在我们已经把括号内的东西当成一个整理处理了,就相 当它是一个变量(我们通过分析这个括号内的东西确实得出了一个指针类型的变量,)于是我们可以把它当成最初的标识符,回到第一步分析,看这个变量(实际上 是把整个括号看成一个标识符,分析这个括号最靠近右边的符号)左边临近的是否是左圆括号或者是方括号,很幸运的,我们发现了左圆括号,就说明这个变量(也 就是整个括号)是一个返回…的函数,而原来那个等价于变量的括号内部事实上是一个指针变量,再结合我们刚才分析出的,就可以知道,这个指针变量指向一个返回…。的函数,最后我们分析这个函数究竟返回什么,这时候只剩下一个char类型符了,所以整个声明的内容是,一个指向函数的指针,该函数返回一个char型的变量分析出这样一个声明并不困难,难就难在用上面的思维进行分析,上面的分析步骤事实上是编译器进行声明分析的步骤,但其实就上面总结的还并不完全。那么下面我就将完整的声明的分析步骤罗列出来:分析的步骤匹配的符号阅读方式1 取最左边的标识符标识符表示标识符是……
2查看标识符右边下一个 [可能的大小对于每一对,表示符号,如果是方括号 ……的数组3 如果是一个左圆括号 ( 可能的参数) 到右括号为止的内容表示返回……的函数4 如果左边的符号是一个 ( 这个左括号把已经处理的左圆括号部分声明组合在一起,直到遇见对应的右括号,然后从第二步 重新开始5 如果左边的符号是下述符号之一*,const,volatile *,const,volatile 继续向左读符号,知道声明器 超出 三者之外,然后重复第4步6 剩下的符号形成基本类型基本类型如char,int 剩余的符号可一并阅读
以上就是声明分析的完整总结版,也是c专家编程书中罗列出来的,我这里只是借鉴一下,供大家学习参考。
好了,有了以上的基础,我们就好分析第一个恐怖型的声明,从这样的声明中,我们上面的分析步骤的优势就体现出来了
void(*signal(int sig, void (*func)(int)))(int);
首先,最左边的标识符是signal,表明signal是…。,紧靠它右边的是一个左圆括号,说明signal是一个返回…的函数,而int sig, void (*func)(int)是这个函数的参数,里面的每一个部分都可以重新用我们的方法进行分析,这里就不叙述了。然后我们根据上面的分析步骤,再看左边的符号是什么,是一个指针符*,表明该函数是一个返回一个指针的函数,此时,函数已经被我们简化成void p(int);j其中p是一个返回指针的函数,是我们上面分析所得出的结果。那么该指针指向的是什么呢,这里再根据前面的步骤,分析整个括号的临近右边的符号,也就是p(我们已经将整个括号中的内容等价为p这个假想中的标识符)右边的符号,发现是一个左圆括号,所以p是一个返回…的函数,而p是一个指针(实际上是某一个函数所返回的指针),所以p是一个指向函数的指针,最后根据void判断出,该函数的返回值为空。这样,一个复杂的声明就被我们抽丝剥茧的分析完了、用完整版的语言来叙述这个声明:这是一个函数,该函数有两个参数(参数的声明分析大家自己完成哈),并且该函数返回一个指针,该指针指向一个函数,该函数有一个参数,并且返回类型为空。
好了,写到这里,希望阅读本文的您已经理解了我所想表达的内容,并且处于对您的尊重,我就将第二个看来更有挑战性的声明交给您来完成。如果有什么问题或者发现本文有什么观点错误的地方,非常欢迎来群里讨论,也十分愿意同您交一个好朋友。
让我们一起学习进步。