C编译器剖析_3.1 语法分析_C语言的表达式(2)

    接下来,我们来分析一下C语言的一元运算表达式,对应的分析函数ParseUnaryExpression()在ucl\type.c中,如图3.1.13所示。由第235至240行的产生式,我们能看到非终结符UnaryExpression有多个侯选式,第254至269行用来处理第237行的侯选式”UnaryOperator  UnaryExpression”。对于表达式”*ptr++”而言,由第236行和第237行的产生式可知,只有分析完了PostfixExpression后缀表达式”ptr++”之后,我们才能得到一元运算符”*”的操作数,进而构成完整的UnaryExpression一元运算表达式”*ptr++”。这实际上意味着在C语言中,形如”++”这样的后缀表达式运算符,要比一元运算符"*"更优先计算的。当然,运算符”++”有”前加加”和”后加加”的区别,例如”++ptr”和”ptr++”,其中,”前加加”是一元运算符,而”后加加”是后缀运算符。在C语言的运算符优先级表中,一般把”++”统一划入“一元运算符”,再规定一元运算符要按从右到左的顺序进行运算,这其实也相当于“后加加”要比其他一元运算符更优先计算。从产生式的角度来理解优先级,则很简单,所有的后缀运算符要先于一元运算符进行计算。同样道理,由C语言标准文法,二元运算表达式BinaryExpression是由一元运算表达式UnaryExpression构成,这意味着只有完成了一元运算表达式的计算,才能进行二元运算表达式的计算,即所有的一元运算符的优先级比二元运算符的优先级更高。简单而言,在C语言运算符优先级上,可粗粗地记为:

后缀运算符     >  一元运算符 > 二元运算符

    如果我们以形如表达式”*ptr++”中的运算数ptr为中心,向后看,则是若干个后缀运算符,这些不同的后缀运算符很自然地就会按从左到右的顺序进行结合;而以运算数为中心,向前看,则是若干个一元运算符,这些不同的一元运算符按从右到左的顺序进行结合,也符合以操作数为中心的视野。如果把ptr看成太阳,那离太阳越近的地方就越温暖,自然就越灿烂。至于不同二元运算符的优先级,估计大部分写了N年代码的C程序员都记不全,我们就按《Thinking in C++》作者Bruce Eckel的建议来做,就是多用小括号。曾经遇到如下所示的一个Bug,某嵌入式C程序员本想通过(*ptr)++来统计脉冲的个数,不过却写成了*ptr++,这两者有完全不同的语义。这样的Bug埋没在几万行的C代码中,看着嵌入式产品每次上电一段时间后就跑飞,实在是让人郁闷的一件事情。让我们还是记住Bruce Eckel的忠告,多用括号吧。

while(1){ 

         *ptr = 0;

         while(flag){

                   *ptr++;     //本来应是(*ptr)++;

         }

}

        

图3.1.13 ParseUnaryExpression()

    图3.1.13中,第235至240的右递归的产生式,实际上暗示了一元运算符的右结合,即要按从右到左的顺序进行计算。第254至261行告诉我们,当一元运算符UnaryOperator是前加加++,前减减--,取地址&,解引用*,正号+,负号-,逻辑非!和按位取反~时,我们会在图中第263至267行进行处理。例如,对于表达式”&a”,我们会为之创建如图3.1.14所示的语法树。


图3.1.14  &a对应的语法树

     图3.1.13第267行的ParseUnaryExpression()是个递归调用,所以我们需要相应的递归出口,由第235至240处的产生式,我们也可以得知,非终结符UnaryExpression的侯选式sizeof(typename)和PostfixExpression实际上就是递归定义的出口。与这两个侯选式对应的代码如图3.1.15所示。


图3.1.15 ParseUnaryExpression_sizeof

    图3.1.15第341行调用ParsePostfixExpression()对后缀表达式进行分析;第300至338行则对由一元运算符sizeof构成的表达式进行分析。按C的语法,以下sizeof表达式中,只有”sizeof  int”是非法的,当类型名typename充当sizeof的操作数时,需要加上一对小括号;当一元表达式UnaryExpression作为sizeof的操作数时,则不一定要加上小括号,如下所示的sizeof a和sizeof(a)都是合法的。

         int  a;

         typedef int  bbb;

         sizeof(a);

         sizeof a;

         sizeof(int);

         sizeof(bbb);

         //sizeof int;

    在图3.1.15第309行,我们遇到的问题是:在识别完sizeof(bbb)的前缀sizeof(之后,我们要判断一下接下来的标志符bbb是一个类型名typename,还是只是一个普通的变量名bbb。第312行的IsTypeName()完成了这个判断。在C语言中,我们可以通过”typedef int bbb;”来为整型int取一个别名bbb,在语法分析阶段,完整的类型系统还没有建立起来,此时我们只需要记录标志符bbb为一个类型名就可以了,UCC编译器会将这些信息记录到向量TypedefNames中。

         static Vector TypedefNames,OverloadNames;

    但是对下面的C程序则言,在函数g内,标志符ccc实际上要当作整型变量来用,而不是用作一个类型名,只有离开函数g之后,标志符ccc才会又充当类型名的角色。此时,我们需要记录标志符ccc在函数g内已被另作他用,UCC编译器会把这个信息记录到向量OverloadNames中。

         typedef int ccc;

         void g(int ccc){

                   //

         }

         ccc  c;


图3.1.16  IsTypename()

     函数IsTypename()的代码如图3.1.16所示。C语言自带的int和doule等关键字是类型名,而通过typedef关键字也可以得到类型名。在图3.1.16第4行,当面对一个标志符TK_ID时,我们需要再调用IsTypedefName(char*id)来判断一下标志符id是否为一个通过typedef关键字定义的类型名。在函数IsTypedefName()中,通过查询在向量TypedefNames中记录的信息就可以完成,第29行用tn->id == id来比较两个标志符的名字是否一样。在“2.5节UCC符号表管理”时,我们介绍过,在UCC编译器中,对于同名的标志符,我们只在内存中记录一份标志符的名字,所以此处我们只要比较两个指针是否一样即可。第12至25行注释中给出的例子,解释了第29行tn->level的作用,在2.5节,我们见过不同level的符号表,UCC通过不同级别的符号表实现了C语言中“作用域scope”的概念,请参见”图2.5.12 多个作用域的符号表”。而第29行的! tn->overload则用来判断类型名是否被重载,例如在上述函数g()中,标志符ccc就被当作变量名之用。 对于一元运算表达式sizeof(a),我们通过图3.1.15的分析函数,可为之创建一棵语法树,如图3.1.17所示。


图3.1.17 sizeof(a)对应的语法树

        

 

    一元运算表达式UnaryExpression还有一个侯选式(type-name ) unary-expression,相应的代码如图3.1.18所示,这个侯选式实际上描述了C语言中的强制类型转换。


图3.1.18 ParseUnarayExpression()_类型转换

     在C语言中,形如”(int)a ”的强制类型转换是以左括号开头的,但是对非终结符UnarayExpression而言,它的侯选式PostfixExpression和( type-name ) unary-expression都可能以左括号开始,在图3.1.18第277行处,我们要判断应选用哪个侯选式,我们还得看一下紧跟在左括号的token是不是类型名,如果是类型名,说明我们要按侯选式(type-name)unaray-expression去分析,如果不是,则词法分析器需要回溯到左括号处,这就是第277行BeginPeekToken()和第295行EndPeekToken()的作用。第283至289行的代码会为”(int) a”创建一个如图3.1.19所示的语法树。第287行的ParseTypeName()用于分析类型名,我们会在后续章节讨论C语言的声明时进行分析,相关代码在ucl\decl.c中。


图3.1.19  (int) a对应的语法树

     分析完ParseUnaryExpression(),让我们再来看一下后缀表达式对应的分析函数ParsePostfixExpression()的部分代码。如图3.1.20所示。


图3.1.20 ParsePostfixExpression()

      由图3.1.20第135至142行,我们可以发现,所有的后缀表达式PostfixExpression都是以基本表达式PrimaryExpression开头的,后面会跟上若干个后缀运算符。C语言中定义了这么几种不同的后缀运算符:数组下标[],函数调用(),成员选择”.”,成员选择”->”,后加加++,和后减减--。第154至164行处理了后缀运算符[]。没有在图3.1.20中给出的函数ParsePostfixExpression()代码,会对除[]之外的另外几个后缀运算符进行分析,所用的套路与[]类似,这里不再重复。仅给出与下面几个后缀表达式对应的语法树,如图3.1.21所示。

         arr[3][5];        a.b.c;        f(30,40,50);      d++;


图3.1.21  后缀运算符对应的语法树

     最后,让我们来看一下基本的表达式PrimaryExpression,图3.1.22给出了与之相关的分析函数ParsePrimaryExpression()的部分代码。


图3.1.22 ParsePrimaryExpression()_ID

     图3.1.22第55至59行的注释告诉我们,基本表达式PrimaryExpression由标志符ID、常量、字符串和加括号的表达式Expression构成。第68至76行的代码为基本的标志识构造了一个语法树结点,其op域记为OP_ID。其余基本表达式的识别如图3.1.23所示。

        

图3.1.23 ParsePrimaryExpression()_STRING_CONST

    图3.1.23第82至106行为整数和浮点数构建了语法树结点,其op域为OP_CONST,常量的值被记录到val域,而整型和浮点数常量的类型则记录到ty域。第108至118行为字符串或宽字符串创建了语法树结点,其op域为OP_STR,其类型ty为字符数组。第120至126行则完成了加括号的表达式的识别。在前文的语法树示意图中,我们已经给出OP_ID和OP_CONST的语法树结点示意图,这里就不再啰嗦。需要注意的是,在目前所得到的关于表达式的语法树上,只有OP_CONST和OP_STR对应结点的类型信息是完整的。语法树中其他结点的类型信息需要我们在语义分析时进行推导。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值