简介:这份常州工学院编译原理试卷A(doc格式,55KB,共1个文件)面向计算机专业学生及备考编译原理课程的读者,用于检验和巩固课程核心知识点。试卷内容覆盖正规表达式与最简DFA构造、逆波兰表示与四元式序列、文法二义性证明与语言描述、First集与Follow集计算、LL(1)文法判定及预测分析表构造,以及if-then-else语句的四元式翻译等典型题型,基本对应词法分析、语法分析、语义分析与代码生成各阶段。资源以单份doc文档呈现,便于打印练习或对照复习,适合作为期末备考、课堂测验与自测模拟的参考材料。目前已有391人学习下载,可帮助读者熟悉常工院试卷风格与命题重点,快速定位薄弱环节并针对性强化训练。
1. 从一份编译原理试卷拆解:词法到代码生成的完整链路
如果你正在准备编译原理的期末复习,或者想找一套能覆盖词法分析、语法分析、语义分析和中间代码生成全链路的练习题,这份试卷的资源价值在于它把课程里最核心的五个模块压缩进了五道大题。正规表达式与最简DFA、逆波兰表示与四元式、文法二义性证明、LL(1)预测分析表构造、if-then-else的四元式翻译,每一道题都对应编译器前端的一个关键阶段。适合两类人:一是正在跟这门课死磕的在校生,需要一套结构清晰的模拟题来检验自己是否真的理解了算法流程;二是已经工作但想回头补编译原理基础的开发者,用试卷里的题目当练习,比看纯理论教材更容易暴露知识盲区。这份资源不是讲义,是一套带明确评分权重的实战题,每道题的分值分布本身就告诉你哪些环节是课程重点。
2. 正规表达式到最简DFA:两道题的完整推导与验证方法
2.1 不以0开头但以11结尾的字符串:从语言描述到DFA
题目要求在字母表{0,1}上构造正规表达式,描述所有不以0开头且以11结尾的字符串。先拆解语言约束:首字符必须是1,末尾两个字符必须是11,中间可以是任意01串。正规表达式可以写成1(0|1)*11。这个表达式的结构是:第一个1锁定首字符,(0|1)*覆盖中间任意长度(包括空串),最后的11锁定结尾。
接下来构造等价的NFA再确定化为DFA。常见做法是先画出NFA状态图:状态q0读入1到q1,q1读入0或1可以自环,q1读入1到q2,q2读入1到q3(接受状态)。确定化时用子集构造法,初始状态{q0},读入1到{q1},读入0到空集。从{q1}出发,读入0到{q1},读入1到{q1,q2}。从{q1,q2}出发,读入0到{q1},读入1到{q1,q2,q3}。从{q1,q2,q3}出发,读入0到{q1},读入1到{q1,q2,q3}。接受状态是包含q3的集合。
最简化时检查等价状态:{q1}和{q1,q2}在输入0时都到{q1},输入1时分别到{q1,q2}和{q1,q2,q3},不等价。{q1,q2,q3}是接受状态,单独一组。最终DFA有四个状态,转移表如下:
| 状态 | 输入0 | 输入1 | 是否接受 |
|---|---|---|---|
| A | 死状态 | B | 否 |
| B | B | C | 否 |
| C | B | D | 否 |
| D | B | D | 是 |
注意:死状态在DFA中通常省略不画,但写转移表时要标注清楚,否则容易在最小化时误判等价。
2.2 包含01子串的字符串:正规表达式与DFA的对应关系
第二题要求构造包含01子串的所有二进制串的正规表达式。这个语言的特点是:只要串中出现过一次连续的01,后面接任意串都满足条件。正规表达式为(0|1)*01(0|1)*。前半部分(0|1)*允许01之前出现任意字符,中间01是必须出现的子串,后半部分(0|1)*允许01之后出现任意字符。
构造DFA时,状态设计要跟踪“是否已经看到01”。初始状态q0表示还没看到01,读入0到q1(看到了0,可能是01的前半),读入1到q0(还是没看到01)。q1读入0到q1(连续0,仍然只看到0),读入1到q2(看到了01,进入接受状态)。q2读入0或1都自环,因为一旦包含01,后续任意字符都不影响结论。这个DFA只有三个状态,最小化时q0和q1不等价(q1读入1到接受状态,q0读入1到自身),q2单独一组。最终状态转移表:
| 状态 | 输入0 | 输入1 | 是否接受 |
|---|---|---|---|
| q0 | q1 | q0 | 否 |
| q1 | q1 | q2 | 否 |
| q2 | q2 | q2 | 是 |
验证方法是拿几个边界串跑一遍:空串不包含01,拒绝;串"01"从q0读0到q1,读1到q2,接受;串"10"从q0读1到q0,读0到q1,最终在q1不是接受状态,拒绝。这种手动验证在考试时能快速检查DFA是否正确。
3. 逆波兰表示与四元式:表达式翻译的两种中间代码形式
3.1 逆波兰表示的手工推导与栈操作验证
题目要求写出A+B*(C-D)+E/(C-D)的逆波兰表示。逆波兰表示也叫后缀表达式,运算符写在操作数之后,不需要括号。手工转换时按运算符优先级和结合性逐步处理:先算括号内的C-D,得到CD-;然后算B*(C-D),得到BCD-*;接着算A+B*(C-D),得到ABCD-*+;再算E/(C-D),得到ECD-/;最后把两部分相加,得到ABCD-*+ECD-/。
用栈验证:从左到右扫描,遇到操作数压栈,遇到运算符弹出两个操作数计算后压回。扫描A压栈,B压栈,C压栈,D压栈,遇到-弹出D和C计算C-D压栈,遇到*弹出(C-D)和B计算B*(C-D)压栈,遇到+弹出B*(C-D)和A计算A+B*(C-D)压栈,E压栈,C压栈,D压栈,遇到-弹出D和C计算C-D压栈,遇到/弹出(C-D)和E计算E/(C-D)压栈,最后遇到+弹出两部分相加。栈最终只剩一个值,验证通过。
3.2 四元式序列的生成规则与临时变量命名
四元式用四个字段表示:运算符、操作数1、操作数2、结果。生成时按逆波兰表示的顺序,每遇到一个运算符就产生一条四元式,临时变量用T1、T2依次编号。对于A+B*(C-D)+E/(C-D),生成过程如下:
(1) - C D T1 (2) * B T1 T2 (3) + A T2 T3 (4) - C D T4 (5) / E T4 T5 (6) + T3 T5 T6注意第4条四元式重新计算了C-D,因为原始表达式中C-D出现了两次,而四元式序列没有做公共子表达式消除。如果题目要求优化,可以把第4条改为(4) = T1 _ T4,直接复用T1的值。考试时如果不确定是否要优化,按最直接的方式生成即可,除非题目明确要求优化。
提示:四元式中临时变量的编号顺序会影响后续代码生成的寄存器分配,实际编译器里会尽量复用临时变量,但试卷题目通常只要求正确生成,不要求优化。
4. 文法二义性与LL(1)分析:从证明到预测分析表构造
4.1 文法二义性证明的两种路径与语言描述
题目给出的文法G(开始符号N)产生式如下:
N → SE | E S → SD | D E → 0 | 2 | 4 | 6 | 8 | 10 D → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9证明二义性需要找到至少两个不同的推导树或最左推导,产生同一个句子。观察文法:N可以推导出SE或E,S可以推导出SD或D,E产生偶数(包括10),D产生单个数字。一个句子比如"10"可以通过N→E→10得到,也可以通过N→SE→DE→1 0得到(这里D→1,E→0,但注意S→D后跟E,实际上S→D产生单个数字,然后E产生另一个数字,拼接成"10")。两条路径产生同一个串"10",但语法树不同,因此文法有二义性。
文法描述的语言是:所有由偶数结尾的数字串,或者更准确地说,所有十进制数字串中最后一个数字是偶数的串。因为E产生偶数(0,2,4,6,8,10),而S产生任意数字序列,N→SE表示任意数字序列后跟一个偶数,N→E表示单个偶数。所以语言L(G) = { w | w是十进制数字串,且w的最后一个字符是偶数 }。注意E中的10是两个字符,但作为整体产生"10",这会导致一些边界情况,比如"10"本身以0结尾,符合条件。
4.2 First集与Follow集的计算过程
题目第四题给出的文法G:
E → TE' E' → +E | ε T → FT' T' → T | ε F → PF' F' → *F' | ε P → (E) | ^ | a | b计算First集:First(E) = First(T) = First(F) = First(P) = { (, ^, a, b }。First(E') = { +, ε }。First(T') = First(T) ∪ { ε } = { (, ^, a, b, ε }。First(F') = { *, ε }。
计算Follow集:Follow(E) = { ), $ }(E是开始符号,$加入;P→(E)中E后面是))。Follow(E') = Follow(E) = { ), $ }。Follow(T) = First(E') \ {ε} ∪ Follow(E) = { +, ), $ }。Follow(T') = Follow(T) = { +, ), $ }。Follow(F) = First(T') \ {ε} ∪ Follow(T) = { (, ^, a, b, +, ), $ }。Follow(F') = Follow(F) = { (, ^, a, b, +, ), $ }。Follow(P) = First(F') \ {ε} ∪ Follow(F) = { *, (, ^, a, b, +, ), $ }。
4.3 LL(1)文法证明与预测分析表构造
证明LL(1)文法需要检查每个非终结符的产生式是否有冲突。对于E→TE',只有一条产生式,无冲突。E'→+E | ε,First(+E)={+},First(ε)={ε},Follow(E')={),$},{+}与{),$}不相交,无冲突。T→FT',单条产生式。T'→T | ε,First(T)={ (,^,a,b },Follow(T')={+,),$},不相交。F→PF',单条。F'→*F' | ε,First(F')={},Follow(F')={ (,^,a,b,+,),$ },不相交。P→(E) | ^ | a | b,各产生式First集分别为{(}、{^}、{a}、{b},互不相交。因此文法是LL(1)的。
构造预测分析表:行是非终结符,列是终结符。对于每个产生式A→α,对每个a∈First(α),在M[A,a]填入A→α;如果ε∈First(α),对每个b∈Follow(A),在M[A,b]填入A→α。最终表如下:
| 非终结符 | ( | ) | + | * | ^ | a | b | $ |
|---|---|---|---|---|---|---|---|---|
| E | E→TE' | E→TE' | E→TE' | E→TE' | ||||
| E' | E'→ε | E'→+E | E'→ε | |||||
| T | T→FT' | T→FT' | T→FT' | T→FT' | ||||
| T' | T'→T | T'→ε | T'→ε | T'→T | T'→T | T'→T | T'→ε | |
| F | F→PF' | F→PF' | F→PF' | F→PF' | ||||
| F' | F'→ε | F'→ε | F'→ε | F'→*F' | F'→ε | F'→ε | F'→ε | F'→ε |
| P | P→(E) | P→^ | P→a | P→b |
注意:T'→T和T'→ε在Follow(T')={+,),$}上,T'→T的First集是{(,^,a,b},与Follow集不相交,所以表中T'行在(、^、a、b列填T'→T,在+、)、$列填T'→ε。F'行类似,列填F'→F',其他列填F'→ε。
5. if-then-else的四元式翻译:控制流与回填技术
5.1 条件语句的四元式生成步骤
题目要求把if x>0 y>0 then z:=x+y else begin x:=x+2; y:=y+3 end;翻译成四元式序列。注意条件部分x>0 y>0在试卷中可能表示x>0 and y>0,这里按逻辑与处理。四元式生成需要用到回填技术:先产生条件跳转指令,但跳转目标暂时留空,等确定目标地址后再回填。
生成过程如下:
(1) > x 0 T1 (2) j= T1 _ 3 // 如果T1为假,跳到第3条之后 (3) > y 0 T2 (4) j= T2 _ 7 // 如果T2为假,跳到else分支 (5) + x y T3 (6) := T3 _ z (7) j _ _ 10 // then分支结束,跳过else (8) + x 2 T4 (9) := T4 _ x (10) + y 3 T5 (11) := T5 _ y (12) ... // 后续语句这里第2条j= T1 _ 3表示如果T1为假(即x>0不成立),跳转到第3条之后的位置,也就是跳过then分支直接去else。第4条类似,如果y>0不成立,跳到第7条之后。第7条是无条件跳转,跳过else分支。注意第8条和第10条的顺序:else分支中先执行x:=x+2,再执行y:=y+3,所以四元式按顺序生成。
5.2 回填技术的实现逻辑与常见错误
回填技术的核心是维护两个列表:真出口链和假出口链。当产生条件跳转时,把跳转指令的编号加入对应的链中;当确定目标位置时,遍历链把目标地址填入。手工做题时,可以用编号代替地址,先写跳转指令,最后统一回填。
常见错误有三个:一是跳转目标算错,比如第2条应该跳到else分支的第一条(第8条),但写成了第3条;二是忘记then分支结束后的无条件跳转,导致执行完then后继续执行else;三是临时变量编号重复,比如T1用了两次。避免方法是每生成一条四元式就检查跳转逻辑,画一个简单的控制流图辅助验证。
提示:四元式中的
j表示无条件跳转,j=表示条件跳转(条件为假时跳转),:=表示赋值。不同教材的符号可能略有差异,但结构一致。
6. 用这套试卷做自测:三个验证技巧与一个血泪教训
6.1 用边界串验证DFA与正规表达式
做完正规表达式和DFA题目后,不要只检查最终答案,拿几个边界串手动跑一遍DFA。比如第一题的语言是“不以0开头但以11结尾”,边界串包括:空串(拒绝)、单字符"1"(拒绝,不以11结尾)、"11"(接受)、"011"(拒绝,以0开头)、"1011"(接受)、"11011"(接受)。如果DFA对"011"返回接受,说明首字符约束没处理好。这种验证方法比重新推导一遍快得多,而且能发现状态转移表中的笔误。
6.2 用栈模拟验证逆波兰表示
逆波兰表示做完后,用栈模拟一遍计算过程。拿ABCD-*+ECD-/为例,从左到右扫描,遇到操作数压栈,遇到运算符弹出两个操作数。如果栈在某个时刻操作数不够弹出,说明表达式写错了。这个方法在考试时能快速检查,比重新推导优先级快。另外注意:逆波兰表示中操作数的顺序和原表达式一致,但运算符的顺序由优先级决定,不要凭感觉调整。
6.3 用预测分析表跑一个输入串
构造完LL(1)预测分析表后,拿一个输入串比如a+a*b跑一遍分析过程。初始栈$E,输入a+a*b$。查表M[E,a]=E→TE',弹出E压入E'T。继续查表,每一步都对照预测分析表。如果某一步查表为空,说明输入串不符合文法或者表构造有误。这个方法能同时验证First集、Follow集和预测分析表的正确性。
6.4 一个血泪教训:不要跳过二义性证明的语法树
我刚开始做这类试卷时,觉得二义性证明就是找两个推导,随便写写就行。结果有一次考试,题目要求“证明文法G有二义性”,我只写了两个最左推导,没有画语法树,扣了一半分。后来才明白,二义性的本质是同一个句子对应两棵不同的语法树,只写推导过程不够直观,阅卷老师要看的是语法树的结构差异。从那以后我每次做二义性证明都强制画两棵语法树,哪怕题目只要求写推导,我也会在旁边附上树形结构。这个习惯让我在后续的编译原理考试里再也没丢过二义性证明的分。希望帮到你。
本文还有配套的精品资源,点击获取