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

C编译器剖析_前言 让我们站在C编译器实现的角度重新认识自己朝夕相伴的C语言,如果你熟悉C语言,并且有基本的数据结构的知识,那就让我们一起开启C编译器剖析这趟旅程。众里寻她千百度,蓦然回首,She is C。那我们就启程吧。 阅读详情

    接下来,我们来分析一下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对应结点的类型信息是完整的。语法树中其他结点的类型信息需要我们在语义分析时进行推导。

C编译器剖析_3.1 语法分析_C语言的表达式(1) 3.1  C语言的表达式     从第3章开始,我们进入UCC编译器的语法分析阶段。相关的代码主要在ucl\decl.c、ucl\expr.c和ucl\stmt.c,分别对应声明Declaration、表达式Expression和语句Statement。在第一章时,我们已经通过一个简单的例子ucc\examples\sc对这些概念有了一个直观的体会,在这一章中,我们需要结合IT大佬们制定的C标准 阅读详情

相关推荐

C编译器剖析_3.2 语法分析_C语言的语句

3.2  C语言的语句_Statement     在这一节中,我们对C语言的各种语句进行分析,相关代码在ucl\stmt.c中。结构体struct astNode是语法树所有结点的父类,而if语句等需要在语法树结点上记录更多的信息,所以在ucl\stmt.h中定义了相关的结构体,我们会在讨论具体的语句时,在语法树示意图上画出相应的结构体。图3.2.1给出了语句的分析函数ParseStateme

SheIsC的专栏 1820

C语言入门(4)------表达式,循环结构

C语言(4) 任务: 1、三元表达式 2、二元表达式 3、循环结构---------------------------------------回顾: 五种选择结构:1、if2、if...else3、if...else if...else4、嵌套if5、switchC语言中的运算式: 1、算术运算式  + -  *  / % 2、赋值运算符  =                       ...

疯狂编程路上的藏羚羊 915

C编译器剖析_3.3 语法分析_C语言的外部声明(1)

3.3  C语言的外部声明_ExternalDeclaration     在分析完表达式和语句之后,《K&R》附录中关于标准C的6页文法,我们就已经差不多已经理解了一半,另外的3页文法基本上都是关于声明Declaration的,与声明相关的代码在ucl\decl.c中。不少编程语言的书籍中,对声明与定义是有严格区分的,在编译器遇到定义时,需要为之分配内存;而遇到声明时,则不必为之分配内存,因为

SheIsC的专栏 1935

SpEL表达式详解

`Spring Expression Language`(简称SpEL)是一种功能强大的表达式语言,是`spring`提供的,该语言类似于JSP当中的EL表达式。但提供了很多额外的功能,最出色的就是`函数调用`和`简单字符串的模板函数`。他需要使用`spring`提供的解析器来解析,但是他不依赖于`spring`,可以独立使用。在`spring`程序当中,我们不用管解析器,由`spring`来帮我们自动构建。我们只需要写想要表达的字符串,交给`spring`来进行解析即可。

怪咖@的博客 3万+

C编译器剖析_3.3 语法分析_C语言的外部声明(2)

在上一小节中,我们讨论了C标准文法的“外部声明ExternalDeclaration”,介绍了“声明Declaration”和“函数定义FunctionDefintion”这两类外部声明,分别举了以下代码作为例子,在图3.3.4和图3.3.7中给出了这两者对应的语法树。 static  const  int  a1= 3, * a2, a3[5],a4(void);//声明Declarati

SheIsC的专栏 1720

C编译器剖析_4.2 语义检查_表达式的语义检查(3)_字符串与标识符

4.2.3             在这一小节,我们先来分析一下基本表达式PrimaryExpression的语义检查,由C的标准文法,我们可以知道与PrimaryExpression相关的产生式如下所示,即加了一对小括号的表达式(Expression)在语法上也相当于标志符ID、常量CONST和字符串StringLiteral。 primary-expression:

SheIsC的专栏 2033

ucc编译器分析与总结(1) 语法分析

编译器、操作系统、数据库是计算机领域中的三大基础软件,基本上所有应用软件都是建立在这三个基础软件之上,这些领域经过前辈们不断打磨现在已经变得非常成熟,很多涉及到的代码技术都是精华中的精华,所以把上面3个软件搞清楚对代码水平的提高会有很多帮助,而且能够对现代计算机的软件架构有更清晰的了解。 我现在要研究一个逻辑推理系统,这个推理系统以C语言为基础,这里必然涉及到C语言的解析问题,所以C语言编译器是必...

test 3201

C编译器剖析_4.1 语义检查_语义检查简介

4.1  语义检查简介     在这一章中,我们需要在语法分析阶段建立的语法树的基础上,进行语义检查。UCC编译器中与此相关的代码主要在ucl\declchk.c,ucl\stmtchk.c和ucl\exprchk.c,分别用于对声明Declaration、语句Statement和表达式Expression进行语义检查SemanticsCheck。其中,最重要的工作就是建立C语言的类型系统,我们

SheIsC的专栏 2350

【C语言】编译和链接深度剖析

本小节,我们学习翻译环境和运行环境,其中我们将学习编译环境的4个阶段:预编译,编译(词法分析,语法分析,语义分析),汇编,链接,文章干货满满!学习起来吧😃!这次阿森和你一起学习了 C语言程序从源代码到可执行文件的整个翻译过程。翻译环境指将源代码翻译成可执行程序的整个过程,运行环境指程序实际执行的环境。预处理(预编译)–>对源代码进行预处理,如宏替换、头文件包含等。编译 -->将预处理后的源代码进行词法、语法和语义分析,生成目标代码。词法分析:识别源代码中的标识符、关键字、运算符等词法单元。

a_sen的博客 1902

C编译器剖析_4.2 语义检查_表达式的语义检查(5)_结构体成员选择

4.2.5         成员选择运算符      在C语言中,结构体struct和联合体union被称为记录类型RecordType,在形如dt.a和ptr->a的后缀表达式中,运算符.和->被称为成员选择运算符。函数CheckMemberAccess()用于对这些表达式进行语义检查,与之相关的代码如图4.2.28所示。在表达式dt.a中,dt和a相当于是运算符.的两个操作数,dt对应的语法

SheIsC的专栏 1729

深入浅出C语言编译器源码剖析

本文还有配套的精品资源,点击获取 简介:编译器是将高级编程语言转换为机器语言的关键工具,本资源提供的“简单编译器”源码用C语言编写,涵盖编译器设计的各个主要阶段,如词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成。通过学习和调试这些源码,开发者能够深入理解编译原理和编程语言的内部工作,提升C语言编程技能,掌握复杂数据结构和算法的应用。建议学习者先了解编译原...

weixin_42451850的博客 1499

C2编程语言编译器内部机制揭秘:从源码到二进制文件的完整流程

C2编程语言编译器(c2c)是一个用C2语言编写的自举编译器,它将C2源代码转换为可执行文件或库。作为C语言的演进版本,C2编译器通过现代化的架构设计实现了高效的编译流程。本文将深入解析C2编译器的工作原理,从词法分析到代码生成,为您揭开编译器内部的神秘面纱。 ## 🔍 C2编译器架构概览 C2编译器采用模块化设计,主要分为以下几个核心组件: - **前端处理层**:负责命令行参数解析和构

gitblog_00567的博客 904

C语言编译器源码项目:编译原理学习与实践

词法分析是编译器的第一阶段,主要任务是将源代码的字符序列转换为一系列的词法单元(也称为记号或tokens)。词法单元是具有特定意义的最小单元,它代表了编程语言的原子符号,例如关键字、标识符、字面量、运算符和分隔符。词法单元可以分为以下几类:关键字(Keywords):如ifelsewhilefor等,这些是编程语言的保留字,具有特定的语法意义。标识符(Identifiers):用于变量名、函数名等,由字母、数字和下划线组成,但第一个字符不能是数字。

weixin_31961675的博客 1043

C编译器剖析_C语言的变参函数

C语言的变参函数     UCC编译器中有不少地方使用了C语言的变参函数,这里我们专门用一小节来对C语言变参函数的实现原理进行分析。C标准库中的printf函数就是一个典型的变参函数,其接口如下所示,函数声明中的省略号…表明这是一个变参函数。          int printf(const char *format, ...);     下面我们举一个简单的例子来说明printf函数的调

SheIsC的专栏 2106

C编译器剖析PDF文档_UCC162.3源代码

包含C编译器剖析PDF文档,及修改少量BUG后的编译器源代码UCC162.3, 欢迎访问对应博客http://blog.csdn.net/sheisc

编译原理 类C语言编译器 词法分析 语法分析LR1 语义分析

用C++编写的类C语言编译器,代码清晰。完成词法分析、语法分析LR1、语义分析。其中LR语法分析自动生成action-goto表。语义分析生成三地址码。可做编译原理课程设计大作业等等。

C编译器剖析_4.2 语义检查_表达式的语义检查(1)

在这一节中,我们来分析一下exprchk.c中的代码。我们通过调用图4.2.1中的函数CheckExpression(),来对表达式所对应的语法树结点进行语义检查。虽然表达式结点对应的kind域皆为NK_Expression,但它们的运算符op域还是各有区别的,由此我们可以为不同的运算符表达式构造不同的语义检查函数,然后把这些函数放置在一张函数表ExprCheckes中,用运算符op作为下标,去调

SheIsC的专栏 1697

c语言表达式的语法分析器

java平台对输入的表达式进行了简单的词法分析(注意:输入只能在txt中输入!!!),用算符优先分析方法分析表达式是否合法,程序能给出语法的分析过程和表达式的结果。由于C语言的运算符较多,导致算符运算关系表比较大,所以每种表达式选择了一种符号作为代表。压缩包中给了多个用例。

上一篇: C编译器剖析_3.1 语法分析_C语言的表达式(1)
下一篇: C编译器剖析_3.2 语法分析_C语言的语句
SheIsC
博客等级 码龄12年 224粉丝 64原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值