编译原理 (二)词法分析、语法分析、语义分析以及中间代码生成器的基本概念

简介: 编译原理 (二)词法分析、语法分析、语义分析以及中间代码生成器的基本概念

1.词法分析

词法分析的过程中,源代码程序被输入到了一个叫做扫描器的东西中,扫描器的任务就是进行词法分析。他应用了一种叫做有限状态机的算法把源代码分割成一个一个的记号,举例比如array[index] = (index + 4) * (2 + 3)这行代码,经过扫描就会变成如下的一个个记号:

记号 类型
array 标识符
[ 左方括号
index 标识符
] 右方括号
= 赋值
( 左圆括号
index 标识符
+ 加号
4 数字
) 左圆括号
* 乘号
( 左圆括号
2 数字
+ 加号
3 数字
) 右圆括号


以上的这些记号一般有以下几类:关键字、标识符、字面量(数字、字符串等)和特殊符号。

单词类型 种别 种别码
关键字 if、else、for…… 一词一码
标识符 变量名、数组名…… 多词一码
常量 整型、浮点型、字符…… 一型一码
运算符 算术(+ - * / %)、关系(> < =)、逻辑(& | ~) 一词一码
界限符 ; ( ) [ ] { } 一词一码


在识别这些标志的同时,扫描器也同时把标识符存放到了符号表,将数字、字符串常量存放到文字表,以备后续步骤使用。对于C语言的预处理,他的宏替换和文件包含等工作不交给编译器范围而是交给独立的预处理器处理。


2.语法分析

语法分析则由分析器去扫描扫描器产生的那些记号去进行语法分析,产生语法树,整个过程采用了上下文无关语法的分析手段,由语法分析树生成的树是以表达式为节点的树,如下所示:

这个数的左分支是array[index]右分支是(index + 4) * (2 + 3)左分支和右分支又可以再拆开,形成语法树

通过语法树我们可以看到很多运算符号的优先级和含义也被确定了下来。对于有多重含义的符号,比如*可以做乘法,也可以作为指针,语法分析阶段,要去确定他们的含义来进行区分,出现了不合法的表示方式,则会抛出语法错误。


3.语义分析

语义分析由语义分析器来完成,语法分析器仅完成了语法的对错,他并不去关心代码实现的含义,在C语言中两个指针相乘是没有意义的,但是在语法层面确是合法的。编译器能分析的语义是静态语义,即编译期可以确定的语义,相反,动态语义则是在运行的时候才确定的语义。


静态语义包括声明类型和类型的匹配、转换。比如当一个浮点数赋值给整型的时候,隐藏了一个过程,就是浮点型到整型转换的过程。但是将浮点数赋值给指针的时候,在语法层面可以,但是语义阶段,会发现,类型不匹配则会报错。

经过语义分析后,整个语法数表达式都被标志了类型,如下:

该表达式中基本所有的类型都是整型,并不需要做类型转换,有些需要做转换的会在语法树上插入转换节点


4.z中间语言生成

目前的编译器都会有很多优化,在源代码中就会有一些优化过程,比如以上的(2+6)就会在编译的时候进行优化,优化后就直接变成了一个数字5

其实直接在语法树上边做优化比较困难,所以源代码优化器往往把整个语法树转换成中间代码,跟目标机器和运行环境无关,他不包含数据的尺寸、变量地址和寄存器的名字。常见的中间代码有三地址码,P-代码等。比如x = y op z,该三地址码表示将变量y和z进行op操作后赋值给x,比如x = y + z;一下是常用的三地址表示方式

指令类型 指令形式
赋值操作 x = y op z 、 x = op y
复制指令 x = y
条件跳转 if x op y goto z
非条件跳转 goto z
参数传递 param z
过程调用 call p, n
过程返回 return x
数组引用 x = y[ i ]
数组赋值 y[ i ] = x
地址以及指针操作 x = &y 、x = *y 、*x = y

我们把上述的例子的语法树翻译成三地址码如下

t1 = 2 + 3
t2 = index + 4
t3 = t1 * t2
array[index] = t3

在三地址码基础上进行优化,会把2+3的结果计算出来,得到t1 = 5然后把t1换成5。这样三地址码就变成了如下

t2 = index + 4
t2 = t2 * 8
array[index] = t2

中间代码把编译器分成了前端和后端(此前后端非彼前后端),前端负责生成与机器无挂的中间代码,后端则是将中间代码变成目标代码。这样对于一些跨平台的编译器而言,可以针对不同平台使用同一个前端,然后对应不同机器开发不同后端。


参考资料《编译原理》、《程序员的自我修养(链接、装载与库)》

相关文章
|
7月前
|
存储 自然语言处理 前端开发
编译原理 - 语义分析
编译原理 - 语义分析
94 1
|
7月前
|
JavaScript 前端开发
解释JavaScript闭包的工作原理,并举例说明其在游戏开发中的应用。
JavaScript闭包允许内部函数访问并保持对外部函数变量的引用,即使外部函数执行结束。当函数返回内部函数时,形成闭包,继承父函数作用域链。在游戏开发中,闭包用于创建具有独立状态和行为的角色实例。例如,`createCharacter`函数创建角色并返回包含属性和方法的对象,内部函数如`getHealth`、`setHealth`和`attack`通过闭包访问并操作角色的变量。这种方式确保了每个角色的状态在不同实例间独立,是实现游戏逻辑的强大工具。
42 2
|
7月前
|
自然语言处理 前端开发 编译器
编译原理 - 语法制导翻译
编译原理 - 语法制导翻译
62 0
|
2月前
|
自然语言处理 编译器 C语言
软考:区分词法分析、语法分析、语义分析
本文解释了编译过程中的词法分析、语法分析和语义分析三个阶段的区别,并提供了相关练习题,帮助读者理解各阶段在编译过程中的作用和重要性。
69 4
|
2月前
|
算法 Java
在Java编程中,关键字和保留字是基础且重要的组成部分,正确理解和使用它们
【10月更文挑战第13天】在Java编程中,关键字和保留字是基础且重要的组成部分。正确理解和使用它们,如class、int、for、while等,不仅能够避免语法错误,还能提升代码的可读性和执行效率。本指南将通过解答常见问题,帮助你掌握Java关键字的正确使用方法,以及如何避免误用保留字,使你的代码更加高效流畅。
34 3
|
6月前
|
自然语言处理 容器
S语言词法分析器设计
还有很多需要优化的地方,作为小白发出了也和大家一起交流下,这次我是分文件写的,因为考虑到以后的实验都用这一套代码,分文件写方便一点,用的是C++14标准
36 0
|
7月前
|
程序员 编译器 C语言
【C 言专栏】C 语言中的预处理器指令
【5月更文挑战第6天】C 语言的预处理器指令在编译前起作用,提供代码灵活性。常见指令包括:`#define`(定义常量和宏)、`#include`(包含文件)、`#if` 等条件编译指令,以及`#pragma`(编译器特定指示)。合理使用能提升代码可读性和可维护性,但过度使用可能导致复杂性增加。注意其文本替换性质及顺序处理,避免头文件重复包含。预处理器在实际应用中用于实现不同功能和配置,是成为优秀 C 语言程序员的关键技能之一。
97 0
【C 言专栏】C 语言中的预处理器指令
|
7月前
根据文法求对应的语言
根据文法求对应的语言
52 0
|
自然语言处理 前端开发 JavaScript
编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(四)结语
编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器(四)结语
88 0
|
7月前
|
XML 数据采集 数据格式
Python语言的结构化标志语言处理
Python语言的结构化标志语言处理