编译原理课程笔记
概论
编程语言的发展:
- 第一代 机器语言: 能够被计算机的硬件系统直接执行的指令程序, 如“
0001000101”。 - 第二代 汇编语言: 将硬件指令用一些助记符表示, 即符号化的机器语言, 如“
ADD,MOV”。 - 第三代 高级语言: 从程序员的角度出发, 对汇编语言进一步抽象, 使用便于理解的“自然语言”表述。
高级语言的实现
编译方式

将⾼级语⾔(源语⾔)翻译成低级语⾔(如汇编语⾔或机器语⾔)的⽬标程序的翻译⽅式。⼀次性将源程序翻译为⽬标程序,之后直接运⾏⽬标程序,⽆需重复翻译。
适⽤场景:程序需要频繁运⾏时,编译⽅式更⾼效,因为它省去了反复翻译的过程。
解释方式

逐⾏翻译并同时执⾏⾼级语⾔程序,翻译与执⾏同步完成。⽆需⽣成独⽴的⽬标程序,边翻译边运⾏。
适⽤场景:程序较简单且需要灵活修改时,例如 Excel 表格中的脚本,解释⽅式更具灵活性。
转换方式

将⼀种⾼级语⾔(A 语⾔)转换为另⼀种⾼级语⾔(B 语⾔),再利⽤ B 语⾔的编译器执⾏。不直接属于⾼级语⾔的实现⽅式,⽽是⼀种变通策略。
适⽤性:依赖已有编译器,适⽤于语⾔间转换的场景。
编译程序的组成

表处理和错误处理也可以算作编译程序的组成部分,但是主要还是中间的六个部分。
词法分析
词法分析器
词法分析器也叫 Scanner,输入是源程序的字符序列,输出是单词序列。它按顺序扫描源程序,识别出具有独立意义的单词,并检查词法错误。
词法分析的输出通常采用二元组形式:<单词类别, 单词内容>。例如:
<保留字, void><界限符, (><保留字, int><标识符, x1><运算符, =><常量, 0>
单词是具有独立含义的最小语义单位。常见单词类型包括保留字、标识符、常量、运算符、界限符、控制符。
实现词法分析器的基本步骤:
- 明确要识别的单词类型和词法规则。
- 使用形式化方法描述各类单词,主要工具是正则表达式和自动机。
- 根据描述设计词法分析算法。
符号和符号串
字母表是元素的非空有穷集合,通常记为 。字母表中的元素称为字母、符号或字符。
例如:
符号串是由字母表中的符号组成的有穷序列,也称字符串或句子。常用 表示。
长度
符号串长度表示为 。空串记为 ,其长度为 。
连接
若 和 都是 上的符号串,则它们的连接记为 。例如 ,,则 。
连接满足:
方幂
符号串的方幂表示重复连接。若 是符号串,则:
符号串集合是由某个字母表上的符号串组成的集合。若 和 是两个符号串集合,则它们的乘积定义为:
例如 ,,则:
符号串集合的方幂定义为:
若 ,则:
正闭包表示一次或多次连接:
星闭包表示零次或多次连接:
也可写为:
例如 letter 表示所有英文字母,则 表示所有非空英文字符串, 表示包含空串在内的所有英文字符串。
正则表达式
正则表达式是描述正则集的一种代数表达式,也称正规表达式。它使用预先定义的符号和运算规则构造模式,用来描述一类字符串。
正则表达式 所描述的符号串集合称为正则集或正规集,记为 ,也称为由 定义的语言。
例如:
这个表达式可描述十进制非负整数。
设 为字母表,正则表达式递归定义如下:
和 是 上的正则表达式:
对任意 , 是 上的正则表达式:
若 和 是正则表达式,则以下表达式也是正则表达式:
它们对应的语言为:
正则表达式中的主要运算:
- 括号
( ):确定运算优先级 - 或运算
|:表示多个模式的并集 - 连接运算
·:表示前后相邻部分的组合,实际书写中常省略 - 闭包运算
*:表示零次或多次重复
实际应用中还常使用正闭包 +:
运算优先级为:
例如 ab* 表示先对 b 做闭包,再与 a 连接,即 。
正则表达式描述单词
正则表达式可用于化简和构造词法规则。
交换律:
结合律:
分配律:
幂等律:
同一律:
例如在字母表 上:
ab* 表示所有以 a 开头,后面跟零个或多个 b 的字符串,即:
a(a|b)* 表示所有以 a 开头,后面跟任意个 a 或 b 的字符串。
整数可用正则表达式描述。设:
则:
- 表示允许前导
0的数字串 - 表示无符号正整数
- 表示带符号整数和
0 - 表示整数
词法分析中常见单词的正则描述:
保留字:
1 | |
标识符:
其中:
整数常量:
其中:
实数常量:
特殊符号包括:
- 运算符:
+|-|*|... - 分界符:
{|}|;|... - 控制符:
\t|\n|...
这几部分的核心关系是:词法分析器要识别源程序中的单词,符号串理论提供基本对象,正则表达式提供形式化描述,单词规则可用正则表达式精确表示。
确定有限自动机 DFA
有限自动机 FA 是一种字符串识别装置,可以识别正规集。词法分析中引入 FA,是为了给词法分析程序的自动构造提供形式化工具。
有限自动机分为两类:
- 确定有限自动机 DFA:Deterministic Finite Automata
- 非确定有限自动机 NFA:Nondeterministic Finite Automata
DFA 定义为一个五元组:
其中:
- 是有穷状态集,其中每个元素称为一个状态
- 是有穷字母表,其中每个元素称为输入字符
- 是唯一初始状态,也称开始状态
- 是状态转换函数,
- 是终止状态集,也称可接受状态集或结束状态集
状态转换函数 表示:当前状态为 ,读入输入字符 a 时,自动机唯一转换到状态 。 称为 的一个后继状态。
例如:
其中转换函数为:
这个 DFA 的初始状态是 ,终止状态是 ,输入字母表是 。
DFA 的表示方式
DFA 有两种常见表示方式。
第一种是状态转换图。状态转换图使用有向图表示自动机:


第二种是状态转换矩阵。矩阵中:
- 行表示状态
- 列表示输入字符
- 矩阵元素表示转换后的状态
- 初始状态常用
+标记 - 终止状态常用
*或-标记
以上面的 DFA 为例,状态转换矩阵为:
| 状态 \ | ||
|---|---|---|
陷阱状态
陷阱状态是自动机进入错误路径后的状态。进入陷阱状态后,后续任意输入通常都停留在陷阱状态中。
例如某 DFA 中:
其中状态 4 就是陷阱状态,因为读入 a 或 b 都回到 4。
DFA 的确定性体现在三个方面:
- 初始状态唯一
- 对任何状态 和输入符号 , 唯一
- 状态转换依赖输入字符,DFA 中每一步都有确定的后继状态
- 的输入为空边,也就是不接受没有任何输入就进行状态转换的情况
DFA 接受的字符串
对于字母表 上的任意字符串 ,若 DFA 中存在一条从初始状态到某个终止状态的路径,并且路径上所有边的标记依次连接后等于 ,则称该字符串可被 DFA 接受或识别。
形式上说,DFA 处理字符串时,从初始状态开始,按字符顺序读取输入,每读入一个字符就根据状态转换函数移动到下一个状态。输入读完后,若当前状态属于终止状态集 ,则该字符串被接受。
DFA 能接受的所有字符串构成的集合,称为 DFA 接受的语言,记为 。
例如某 DFA 的路径可识别形如 aba(ab)* 的字符串,则:
aba可被接受abaab可被接受abaabab可被接受abaa根据最终状态判断- 是否被接受取决于初始状态是否为终止状态
DFA 的应用包括:
- 在语言学中,自动机可作为语言识别器
- 在计算机科学中,自动机可作为计算过程的动态数学模型
- 在自动控制领域中,自动机可处理控制信号序列
复印机工作过程可用状态转换图描述:
- 启动命令使复印机从开始状态进入等待状态
- 复印命令使其从等待状态进入复印状态
- 复印完成后回到等待状态
- 关机命令使其进入结束状态
- 复印时没纸则进入缺纸状态,装纸后回到等待状态
- 复印时卡纸则进入卡纸状态,故障排除后回到等待状态
这个例子说明:自动机的状态可表示系统所处阶段,输入事件可触发状态转换。
设计识别能被 3 整除的二进制数的 DFA 时,可用余数作为状态:
- :当前二进制数除以
3余0 - :当前二进制数除以
3余1 - :当前二进制数除以
3余2
初始状态是 ,终止状态也是 。因为余数为 0 表示该二进制数能被 3 整除。
读入一个新二进制位 b 后,原数 变为 ,新余数为:
其中 是原来的余数,。
因此转换规律为:
| 状态 | 0 | 1 |
|---|---|---|
DFA 描述单词
DFA 可以用来描述词法分析中的各种单词。
标识符的描述,设:
标识符的正则表达式为:
对应 DFA 的含义是:第一个字符必须是字母或下划线,后续字符可以是字母、下划线或数字。常数等的命名规则同上。
这些表达式可转成 DFA,用状态表示是否已经读入符号、整数部分、小数点和小数部分。
特殊符号也可由 DFA 描述。例如 {、+、>、= 都可以从初始状态读入单个字符后进入对应终止状态。若要识别复合运算符,例如 >=,则读入 > 后需要继续判断下一个字符是否为 =。
保留字可用字符路径描述。例如 for 的 DFA 路径为:
1 | |
其中状态 3 是接受状态。
if 的 DFA 路径为:
1 | |
其中状态 5 是接受状态。
实际词法分析中,保留字和标识符容易冲突。常用处理方法是先按标识符规则识别完整字符串,再查保留字表。若识别到的字符串是 if、for、while 等,则输出保留字类别;否则输出标识符类别。
DFA 的程序实现
DFA 的程序实现主要有两种方法。
第一种是直接转向法。它基于状态转换图实现,每个状态对应一段 switch 判断,不同输入字符跳转到不同状态。
示意代码:
1 | |
直接转向法的特点是:程序结构与状态图一一对应,理解直观;当状态图变化时,代码也要跟着变化。
第二种是基于状态转换矩阵的方法。它把 DFA 存成表,通过查表完成状态转换。
示意代码:
1 | |
其中:
state表示当前状态curChar表示当前输入字符T[state][curChar]表示状态转换矩阵FinalState表示终止状态集合#表示输入结束符
状态转换矩阵法的优点是程序框架固定,只需要修改状态表即可适配不同 DFA。它适合词法分析器的自动生成。状态较多、字符集较大时,矩阵可能占用较多存储空间,可使用压缩表或稀疏表优化。
NFA 的确定化
NFA,即非确定有限自动机,是一个五元组:
其中:
- :有穷状态集
- :输入字母表
- :状态转换函数,
- :初始状态集
- :终止状态集
含义是:NFA 在某个状态读入一个字符,或通过 空边,可以转移到一个状态集合。它允许一个状态对同一输入有多个后继状态,也允许 转移。
自动机等价的定义:设 和 是同一个字母表 上的自动机,若它们接受的语言相同,即:
则称 和 等价。
重要定理:对任意一个 NFA ,都存在一个 DFA ,使得:。
由 NFA 构造与其等价的 DFA 的过程,称为 NFA 的确定化。
NFA 确定化的核心方法是子集法。它的思想是:DFA 的一个状态记录 NFA 在读入某个输入符号后可能达到的一组状态。也就是说,NFA 中的一组状态:
可以作为 DFA 中的一个状态 。
无 空边 NFA 的确定化
对于无 空边的 NFA,确定化时直接计算状态集合在每个输入符号下能到达的新状态集合。
设当前 DFA 状态对应 NFA 状态集合 ,输入字符为 ,则新状态集合为:
每得到一个新的状态集合,就把它作为 DFA 的一个新状态,继续对所有输入符号求转换,直到没有新状态集合产生。
带 空边 NFA 的确定化
带 空边时,需要先引入 闭包。状态集 的 闭包记为:
定义如下:
- 若 ,则 。
- 若 ,并且从 出发经过任意条 边可到达 ,则 。
设 是 NFA 的一个状态集合,对输入字符 ,先求:
再求:
其中 就是状态集 读入 a 后在 DFA 中应转移到的新状态集合。
完整确定化流程:
- 求 NFA 初始状态集的 闭包,作为 DFA 初始状态。
- 对每个 DFA 状态集合 ,分别计算每个输入字符下的 。
- 若 是新的状态集合,则加入 DFA 状态集。
- 重复计算,直到状态集合不再增加。
- 若某个 DFA 状态集合中包含 NFA 的终止状态,则该 DFA 状态是终止状态。
课件中的例子把 NFA 状态集合转换为 DFA 状态,例如:
其中包含 NFA 终态 9 的集合,对应 DFA 的终止状态。
NFA 确定化的本质是把“多种可能路径同时存在”的情况压缩进 DFA 的一个集合状态中。每个集合状态代表 NFA 当前可能处于的所有状态。
DFA 的化简
DFA 化简的目标是得到最小自动机。最小自动机的定义是:DFA 中没有无关状态,也没有等价状态。
无关状态
状态 是无关状态,通常有两种情况:
- 从开始状态没有到 的通路。
- 从 出发没有到任意终止状态的通路。
这类状态对识别语言没有实际贡献,可以删除。
等价状态
对 DFA 中两个状态 和 ,若分别把它们看作初始状态时,能接受的符号串集合相同,则称 和 等价。
状态等价需要满足两个条件。
- 一致性条件: 和 同时为接受状态,或同时为非接受状态。
- 蔓延性条件:对所有输入符号, 和 都必须转换到等价状态中。
也就是说,若输入字符为 ,则 和 也应等价。
终止状态和非终止状态属于不同类别,二者在初始划分时分开。
状态分离法
DFA 化简常用状态分离法。步骤:
- 初始划分:把所有终止状态分为一组,所有非终止状态分为一组。
- 检查每一组中的状态:若它们对某个输入字符转向不同的组,则这些状态应被分离。
- 得到新分组后继续检查。
- 重复分离,直到没有新组产生。
- 每个最终分组中的状态互相等价,可以合并为一个状态。
课件例子中的初始划分为:
经过输入字符 a 和 b 的转向检查后,进一步分离为:
最终化简后,这三个状态集合分别合并为三个新状态:
DFA 化简的关键判断是:同组状态在每个输入字符下的后继状态是否仍然落在相同分组中。若转向模式相同,则这些状态保留在同组;若转向模式不同,则继续拆分。
正则表达式和有限自动机的相互转化
正则表达式和有限自动机描述能力等价。课件给出的定理是:对 上的每一个正则表达式 ,都存在一个 上的 NFA ,使得:
因此,正则表达式可以转为 NFA,NFA 也可以转为等价的正则表达式。
正则表达式到 NFA
正则表达式到 NFA 的转换基于基本结构组合。
- 单个字符
a:构造一条从开始状态到终止状态、标记为a的边。 - 连接表达式
ab:先构造a的 NFA,再构造b的 NFA,把前一个片段的终止状态连接到后一个片段的开始状态。 - 或表达式
a|b:从新开始状态通过 边分别进入a和b的子自动机,再通过 边汇合到新终止状态。 - 闭包表达式
b*:构造b的子自动机,并加入 边,使其可以重复执行,也可以直接从开始状态到终止状态。
课件例子:为正则表达式 (a|b)*aa 构造 NFA。
拆分结构为:
1 | |
再继续拆为:
1 | |
构造思路:
- 先构造
a|b的分支结构。 - 对
a|b加*,形成(a|b)*的循环结构。 - 后面依次连接两个
a。 - 得到接受所有以两个
a结尾、前面由任意个a或b组成的字符串的 NFA。
该表达式对应的语言可以理解为:
即所有字母表 上以 aa 结尾的字符串。
NFA 到正则表达式
NFA 到正则表达式的转换可通过逐步消去状态完成。基本思想是把经过中间状态的路径合并为一个正则表达式标记。
常见合并规则:
- 多条并行边合并为或表达式,例如
a和b合并为a|b - 连续路径合并为连接表达式,例如
a后接b合并为ab - 自环合并为闭包表达式,例如状态上有
a自环,可形成a* - 经由某个中间状态的路径可合并为“进入路径 + 自环闭包 + 离开路径”
课件例子中,NFA 逐步消去中间状态后得到正则表达式:
这个表达式表示:
- 字符串以
a开始。 - 中间接
ab或ba。 - 后面接零个或多个
a。 - 最后以
b结束。
构造 (a|b)(c|d)*(e|f) 的最简 DFA
目标正则表达式:
它描述的字符串结构为:
- 第一个字符是
a或b。 - 中间可以有零个或多个
c或d。 - 最后一个字符是
e或f。
第一步:转为 NFA。
结构可分为三段:a|b、(c|d)*、e|f
NFA 中先通过 a 或 b 进入中间部分,中间部分在 c 和 d 上循环,最后通过 e 或 f 到达终止状态。
第二步:NFA 确定化。
课件给出的 DFA 状态集合包括:
其中 是初始状态, 是终止状态。
对应转换表可整理为:
| 状态 | a | b | c | d | e | f |
|---|---|---|---|---|---|---|
第三步:DFA 最小化。
初始划分:
进一步判断后得到等价状态:
因此 和 可以合并。
最简 DFA 的结构为:
- 初始状态
- 中间状态
- 终止状态
转换关系:
- 读入
a或b到达 - 读入
c或d仍留在 - 读入
e或f到达
第 9 到第 11 部分可以串成一条完整流程:先把 NFA 通过子集法确定化为 DFA,再用状态分离法化简 DFA;正则表达式可先转 NFA,再确定化、化简,最终得到可直接实现的最简 DFA。