1. 这份笔记不是“背诵清单”,而是程序设计语言底层逻辑的通关地图
“软件设计师【软考中级】复习笔记 —— 第八章(程序设计语言与语言处理程序基础)”这个标题,乍看是考试资料,但实际藏着一条被多数考生忽略的关键路径:它不考你记住多少名词解释,而是检验你能否在脑中构建出“代码如何变成机器指令”的完整执行链路。我带过几十位备考者,发现一个高度一致的现象——凡是卡在第八章、反复错编译原理相关题目的人,问题从来不在“记不住”,而在于脑子里缺一张图:一张从源代码敲下第一个字符开始,到CPU真正执行加法指令为止,中间每一步发生了什么、谁在控制、数据怎么流动、错误在哪一环暴露出来的动态流程图。这一章覆盖的词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成,不是六个孤立概念,而是同一台精密流水线上的六个工位。比如,当你写int a = b + c * 2;,词法分析器只负责切出int、a、=、b、+、c、*、2这些“零件”;而语法分析器立刻要判断这些零件能不能按C语言的语法规则拼成合法结构——它得识别出*的优先级高于+,所以c * 2必须先组合;语义分析器则进一步检查b和c是否已声明、类型是否支持加法;中间代码生成阶段,这行代码很可能被翻译成三地址码t1 = c * 2; t2 = b + t1; a = t2;最后目标代码生成器才把t1 = c * 2拆解为加载c到寄存器、加载常数2、执行乘法指令、存回寄存器等一系列真实CPU操作。整套流程环环相扣,漏掉任一环节的理解,遇到“以下哪项属于语法分析任务?”这类题就会犹豫。这份笔记的核心价值,就是帮你把教科书上扁平化的文字描述,还原成可触摸、可推演、可调试的立体认知模型。它适合两类人:一类是正在啃教材却总感觉“懂了又像没懂”的应试者,另一类是刚转行做开发、想补足计算机系统底层常识的实践者。它不替代刷题,但能让你每道题都刷得明白;它不教你写代码,但能让你写的每一行代码,在脑中都有清晰的执行回响。
2. 内容整体设计与思路拆解:为什么放弃“定义罗列”,选择“流程驱动”式重构
2.1 放弃传统复习路径的三个致命缺陷
市面上绝大多数软考复习资料,对第八章的处理方式高度同质化:开篇直接抛出“词法分析:将源程序分解为单词符号序列”,接着是“语法分析:根据语法规则检查单词序列的结构合法性”,再是“语义分析:检查程序的含义是否正确”……这种纯定义堆砌模式,我在辅导中亲眼见证其失效过程。某位有三年Java开发经验的学员,能熟练写出Spring Boot微服务,却在模拟题中连续三次选错“语法错误和语义错误的区别”。他告诉我:“老师,我知道if (x = 5)是语义错误,因为=是赋值不是比较,但题目问‘编译器在哪一阶段报错’,我就懵了。” 这个案例直指传统路径的三大硬伤:
割裂性:把六个阶段强行切成六个独立模块,导致学习者无法建立时序关联。现实中,语法分析器发现
if (x = 5)时,会按语法规则接受它(因为if (表达式)是合法语法),只有到语义分析阶段,才会检查表达式x = 5的返回值类型是否为布尔型,从而报错。若不理解这个先后依赖,就永远分不清错误归属。静态性:定义描述全是“是什么”,缺乏“怎么做”的动态过程。例如,“中间代码生成”被简化为“生成三地址码”,但没人告诉你:为什么是三地址码?两地址不行吗?四地址呢?答案藏在硬件限制里——现代CPU寄存器数量有限,三地址码(如
a = b op c)能最大限度减少寄存器分配压力,同时保持计算逻辑清晰。这个“为什么”,才是考试命题人最爱挖的坑。抽象性:所有术语都悬浮在真空中。说“语法分析使用LL(1)或LR(1)分析器”,却不展示一个真实C语言
while循环的语法树如何被逐步构建出来。没有具象锚点,记忆就成了无根浮萍。
2.2 “流程驱动”设计的底层逻辑:以一次真实编译过程为唯一主线
本笔记彻底抛弃章节式切割,代之以单次编译全流程穿透式讲解。我们选定一个极简但具备典型特征的代码片段作为贯穿始终的“实验体”:
int main() { int x = 10; float y = 3.14; if (x > 5) { y = y * 2.0; } return 0; }这个例子虽短,却精准覆盖第八章全部核心考点:
- 词法分析:需识别整型字面量
10、浮点字面量3.14、标识符x/y、运算符>/*、关键字int/float/if/return; - 语法分析:需构建包含函数定义、变量声明、条件语句、表达式嵌套的完整语法树,其中
x > 5是关系表达式,y * 2.0是算术表达式,二者嵌套层级不同; - 语义分析:需检查
x > 5中x是否已声明(是)、类型是否支持比较(int支持)、y * 2.0中y(float)与2.0(double)的隐式类型转换规则; - 中间代码生成:需将
if语句翻译为带跳转标签的三地址码,体现控制流结构; - 代码优化:可演示常量折叠(
2.0是常量,但此处无优化空间)、死代码消除(本例无)等基础优化; - 目标代码生成:需映射到x86-64汇编指令,如
movl $10, %eax(加载整数)、cvtsi2ss %eax, %xmm0(整型转单精度浮点)。
选择此路径的深层理由在于:软考命题本质是能力验证,而非知识检索。历年真题中,超过70%的第八章题目都以“给定一段代码,问在XX阶段发生XX现象,原因是什么”为模板。例如:“某程序在编译时报告‘未声明的标识符z’,该错误最可能在哪个阶段被检测到?”——答案是语义分析阶段,但前提是考生脑中必须有“词法分析只认字符序列、语法分析只管结构、语义分析才查符号表”这条清晰的时间线。流程驱动法,正是用一次完整的、可推演的编译旅程,把这条时间线刻进你的神经回路。
2.3 工具选型与可视化策略:用最小成本构建最大认知锚点
为避免陷入纯理论空谈,笔记全程绑定两个轻量级工具,它们不用于考试,但极大提升理解效率:
JFLAP(Java Formal Languages and Automata Package):一个开源的、无需安装的Web版自动机模拟器。我们用它亲手绘制并运行一个简化版的C语言词法分析器状态转换图。例如,针对标识符识别,我们定义:起始状态S0,接收字母或下划线进入S1,S1接收字母、数字或下划线保持在S1,遇到空白或运算符则进入终态S2。当输入
my_var123时,JFLAP会高亮每一步状态迁移,直观展示“为什么123abc不是合法标识符(数字开头)”。这种动手操作,比背十遍定义都管用。TINY Compiler(教学编译器):一个仅500行C代码的极简编译器框架,专为教学设计。我们不求跑通全功能,只提取其语法分析模块,用它解析前述
main函数示例,并打印出生成的抽象语法树(AST)文本。你会看到类似这样的输出:FunctionDef: main └── Block ├── VarDecl: x (int) │ └── IntLiteral: 10 ├── VarDecl: y (float) │ └── FloatLiteral: 3.14 ├── IfStmt │ ├── RelExpr: > │ │ ├── Id: x │ │ └── IntLiteral: 5 │ └── Block │ └── Assign: y │ └── BinOp: * │ ├── Id: y │ └── FloatLiteral: 2.0 └── ReturnStmt └── IntLiteral: 0这棵树就是语法分析的“产物”,也是语义分析的“输入”。看到
IfStmt节点下挂载着RelExpr和Block,你就彻底明白“语法分析负责搭骨架,语义分析负责填血肉”的比喻所指为何。
这两个工具的选择逻辑很务实:零学习成本(JFLAP开网页即用,TINY代码注释详尽)、结果即时可见(状态迁移高亮、AST树形打印)、完全贴合考点(词法状态机、AST结构正是高频考题)。它们不是炫技,而是为你在抽象概念和具象结果之间,架起一座可触摸的桥。
3. 核心细节解析与实操要点:拆解六大阶段的“不可见战场”
3.1 词法分析:字符序列到单词符号的“第一次提纯”,重点在边界判定
词法分析是编译的第一道闸门,它的任务看似简单——把源代码字符串切分成有意义的“单词”(Token),如关键字、标识符、常量、运算符、分隔符。但“看似简单”背后,是大量精妙的边界判定规则,而这恰恰是考试陷阱高发区。以C语言为例,我们用JFLAP模拟一个关键冲突场景:==与=的识别顺序。
很多初学者认为“==是一个整体”,但词法分析器的真相是:它采用最长匹配原则(Longest Match Rule),且按预定义的优先级顺序扫描。假设我们的词法规则按如下顺序排列(这是标准C编译器做法):
==(关系运算符)=(赋值运算符)>(大于运算符)
当扫描器读入输入流a == b时,它从左到右逐字符推进:
- 读到
a,匹配标识符规则,生成 TokenID(a); - 读到空格,结束标识符;
- 读到
=,此时它不会立即停在这里生成ASSIGN,而是“贪心地”继续读下一个字符; - 下一个字符是第二个
=,于是==完整匹配,生成 TokenEQ(等于); - 后续空格、
b同理。
但如果规则顺序写反了(如把=放在==前面),扫描器在读到第一个=时就会立即生成ASSIGN,导致==永远无法被识别——这正是某些自制编译器出现诡异bug的根源。软考真题曾直接考查此点:“若词法分析器按顺序尝试匹配规则,且将‘=’规则置于‘==’规则之前,则源代码‘a == b’将被错误解析为?” 答案是ID(a), ASSIGN, ID(b),即两个标识符加一个赋值符,完全丢失了相等比较的语义。
另一个易错点是数字字面量的类型判定。C语言中123是int,123L是long,123.0是double。词法分析器必须在切分时就附带类型信息,因为后续阶段(尤其是语义分析)需要它。例如,float f = 123.0F;中的123.0F,词法分析器需标记为FLOAT_LITERAL并注明后缀F(单精度)。若它只输出123.0而不带精度标识,语义分析器就无法判断f的赋值是否安全(double赋值给float需警告)。因此,一个合格的词法分析器输出,绝不仅是单词列表,而是<TokenName, Lexeme, Attribute>三元组,其中Attribute就是类型、值、行号等关键属性。
提示:考试中遇到“词法错误”题,优先检查三类边界:1)标识符是否以数字开头;2)字符串常量是否缺少右引号;3)注释是否未闭合(
/*开始但无*/结束)。这些是词法分析器最先崩溃的点。
3.2 语法分析:从线性字符流到树形结构的“空间跃迁”,核心在文法建模
如果说词法分析是“一维切片”,那么语法分析就是“二维建模”——它要把线性的Token序列,组织成反映程序结构的树形(AST)或图状(CFG)结构。其核心依赖是上下文无关文法(CFG),而CFG的书写质量,直接决定语法分析的成败。我们以if语句为例,对比两种文法写法的致命差异:
错误文法(导致二义性):
Statement → IfStmt | OtherStmt IfStmt → 'if' '(' Expr ')' Statement | 'if' '(' Expr ')' Statement 'else' Statement这个文法看似合理,但它存在著名的“悬空else(Dangling Else)”问题。对于代码if (a) if (b) s1; else s2;,它可以被解析为:
- 方案A:
else s2属于外层if (a); - 方案B:
else s2属于内层if (b)。
两种解析都符合文法,编译器无法抉择,这就是二义性。而标准C语言规定else总是匹配最近的未配对if(即方案B)。要消除二义性,必须重写文法:
正确文法(强制匹配最近if):
Statement → MatchedStmt | UnmatchedStmt MatchedStmt → 'if' '(' Expr ')' MatchedStmt 'else' MatchedStmt | OtherStmt UnmatchedStmt → 'if' '(' Expr ')' Statement | 'if' '(' Expr ')' MatchedStmt 'else' UnmatchedStmt这个文法通过区分MatchedStmt(已配对的语句)和UnmatchedStmt(未配对的语句),强制else只能与UnmatchedStmt结合,从而在文法层面就杜绝了歧义。考试中常考“以下文法是否二义”,解题关键就是尝试构造一个句子,看它能否生成两棵不同的语法树。若能,即为二义。
语法分析器的实现算法,主要分两大流派:自顶向下(Top-Down)和自底向上(Bottom-Up)。软考侧重前者,尤其LL(1)分析器。LL(1)的名字含义是:第一个L表示从左到右扫描输入,第二个L表示产生最左推导,(1)表示只需向右看1个Token即可决定选用哪个产生式。其核心数据结构是预测分析表(Predictive Parsing Table)。我们以一个极简的算术表达式文法为例:
E → T E' E' → + T E' | ε T → F T' T' → * F T' | ε F → ( E ) | id构建预测分析表时,关键步骤是计算每个非终结符的FIRST和FOLLOW集合。例如,E'的FIRST集是{+, ε}(因为它能推出+TE'或空),FOLLOW(E')是{), $}($表示输入结束)。当分析器看到输入为+且当前栈顶是E'时,查表得动作是E' → + T E';当看到)时,查表得动作是E' → ε(即弹出E')。这个表就是编译器的“决策大脑”,考试常考“表中某位置应填什么产生式”,解题必须手算FIRST/FOLLOW。
注意:
LL(1)要求文法必须满足两个条件:1)无左递归;2)对每个非终结符A,若A → α | β,则FIRST(α)与FIRST(β)不相交,且若α或β可推出ε,则FIRST(α) ∩ FOLLOW(A) = ∅。这是判断文法能否用LL(1)分析的黄金法则。
3.3 语义分析:为语法树注入“生命”的“意义审查官”,难点在符号表与类型系统
语法分析产出的是一棵“骨架”(AST),语义分析则是为其填充“血肉”(类型、作用域、存储属性)的过程。其核心挑战在于符号表(Symbol Table)的设计与管理。符号表不是简单的哈希表,而是一个支持嵌套作用域(Nested Scopes)的动态结构。以我们的main示例为例:
- 全局作用域:空;
main函数作用域:插入x(int)、y(float);if语句块作用域:理论上可定义新变量,但C89不支持,故此处无新符号。
当语义分析器遍历AST,到达y = y * 2.0时,它需要:
- 在当前作用域(
if块)查找y,未找到; - 向上查找至
main作用域,找到y,类型为float; - 查找
2.0,词法分析已标记为double字面量; - 执行类型检查:
float与double的乘法是否允许?C语言规定,float会先提升(Promote)为double,再进行运算,结果为double; - 但赋值语句要求左边
y(float)能接收右边double值,这属于窄化转换(Narrowing Conversion),编译器应发出警告(或错误,取决于严格级别)。
这个过程揭示了语义分析的两大支柱:作用域链(Scope Chain)和类型兼容性规则(Type Compatibility Rules)。考试中高频考点是“变量未声明”、“类型不匹配”、“重复声明”等错误的检测阶段,答案必然是语义分析。但更深层的题是:“以下哪种情况,语法分析能通过,但语义分析会报错?” 正确选项一定是涉及跨作用域引用或类型隐式转换失败的场景,如int x = 5; { int y = x + 10; } printf("%d", y);—— 外层访问内层变量y,语法上printf的参数是合法表达式,但语义上y不在作用域内。
符号表的实现细节也常被考查。例如,一个健壮的符号表必须记录每个符号的:
- 名称(Name)
- 类型(Type,如
int,struct node*) - 存储类别(Storage Class,如
auto,static,extern) - 作用域深度(Scope Level,用于快速定位)
- 内存偏移(Offset,为代码生成准备)
当遇到static int count = 0;时,符号表不仅要存count,还要标记其Storage Class为static,这意味着它将在数据段(.data)而非栈上分配内存,且生命周期贯穿整个程序运行期。这个信息,语法分析器根本无法提供,唯有语义分析能赋予。
3.4 中间代码生成:架设在高级语言与机器指令间的“通用桥梁”,精髓在三地址码的普适性
中间代码是编译器的“普通话”,它屏蔽了源语言(C/Java)和目标机器(x86/ARM)的双重差异,让优化和代码生成可以独立进行。软考中最常考的中间代码形式是三地址码(Three-Address Code, TAC),其格式统一为x = y op z(最多三个地址),如t1 = a * b,t2 = t1 + c,d = t2。为什么是“三地址”?因为它是在保证计算逻辑完整性和寄存器分配效率之间取得的最佳平衡点。
我们来对比其他可能形式:
- 一地址码(如汇编的
ADD A, B):操作隐含在指令中,难以做通用优化(如a*b + a*c无法轻易提取公因子a*(b+c)); - 二地址码(如
MOV A, B):仍需频繁覆盖操作数,不利于并行优化; - 四地址码(如
ADD R1, R2, R3, R4):虽清晰,但现代CPU寄存器数量有限(x86-64约16个通用寄存器),四地址会极大增加寄存器分配压力。
三地址码的精妙在于:每个语句只有一个左值(x),右值(y,z)可为变量、常量或临时变量(t1,t2),这使得数据流分析(Data Flow Analysis)成为可能。例如,对t1 = a * b; t2 = t1 + c; d = t2;,优化器可以轻松识别t1和t2是临时变量,若后续不再使用d,则整个计算链可被删除(死代码消除)。
生成三地址码的关键是遍历AST并为每个节点生成对应代码。以if (x > 5) { y = y * 2.0; }为例,其AST中IfStmt节点的子节点是RelExpr和Block。生成规则是:
- 为
RelExpr生成比较代码:t1 = x > 5,结果为布尔值; - 为
t1生成跳转:ifFalse t1 goto L1(若t1为假,跳转到L1); - 为
Block中的赋值生成:t2 = y * 2.0; y = t2;; - 在
Block结束后添加标签L1:。
这里L1就是控制流标签(Control Flow Label),它不对应任何源代码,却是连接高级控制结构(if)与底层跳转指令(jmp)的纽带。考试中常给出一段TAC,问它对应源代码的哪种结构,解题关键是识别ifFalse/ifTrue与goto的配对模式,以及标签的嵌套关系。
实操心得:手动生成TAC时,务必为每个临时变量(
t1,t2...)编号,并确保其作用域清晰。我曾见过考生把t1在不同分支中重复使用,导致逻辑混乱。一个简单技巧是:为每个表达式分配唯一t变量,如x > 5生成t1,y * 2.0生成t2,绝不混用。
3.5 代码优化:在不改变语义前提下的“性能精雕”,重点识别可量化收益的变换
代码优化不是玄学,而是基于数据流分析和控制流分析的确定性变换。软考不要求你写优化器,但要求你识别哪些变换是合法的、哪些是危险的。我们聚焦三个最常考的优化类型:
1. 常量折叠(Constant Folding):在编译期计算常量表达式。例如int a = 2 + 3 * 4;,优化后直接生成int a = 14;。其合法性基于:所有操作数都是编译期已知常量,且运算无副作用(如不调用函数)。考试陷阱在于:int a = 2 + 3 * 4 + func();不能折叠,因为func()的调用时机和返回值未知。
2. 公共子表达式消除(Common Subexpression Elimination, CSE):识别重复计算。例如:
a = b * c + g; d = b * c * e;b * c被计算两次。优化后:
t1 = b * c; a = t1 + g; d = t1 * e;其前提是b和c在两次计算间未被修改(即“活跃性”分析确认)。若中间有b = 10;,则CSE非法。
3. 循环优化:强度削弱(Strength Reduction):用廉价操作替代昂贵操作。最经典例子是数组索引:a[i]在x86上通常编译为base + i * sizeof(int)。乘法代价高,可替换为加法:
// 原循环 for (i = 0; i < n; i++) { sum += a[i]; } // 优化后 addr = &a[0]; // base address for (i = 0; i < n; i++) { sum += *addr; addr += sizeof(int); // 加法替代乘法 }这个变换的收益是可量化的:现代CPU中,整数加法延迟约1周期,整数乘法约3-10周期,差距显著。
考试中常问“以下哪种优化一定不改变程序语义?”,正确答案必然是上述三种之一。而干扰项往往是窥孔优化(Peephole Optimization)的误用,如“将x = x + 0删除”,这看似安全,但若x是volatile变量(如硬件寄存器映射),则删除会改变语义(失去读取寄存器的副作用),故非法。
3.6 目标代码生成:将中间表示落地为CPU指令的“最后一公里”,核心是寄存器分配与指令选择
目标代码生成是编译的终点,也是最贴近硬件的一环。其两大核心任务是:指令选择(Instruction Selection)和寄存器分配(Register Allocation)。我们以t1 = y * 2.0;(其中y是float)为例,看x86-64汇编如何生成:
指令选择:y * 2.0是浮点乘法,x86-64的SSE指令集提供mulss(Scalar Single-Precision Multiply)指令,操作数为XMM寄存器。因此,选择mulss %xmm0, %xmm0(假设y已在%xmm0)。
寄存器分配:这是NP难问题,实践中多用图着色(Graph Coloring)算法。基本思想是:为每个变量/临时变量创建一个节点,若两个变量在程序中“活跃期重叠”(即同时需要被保存),则在图中连一条边。然后用尽可能少的颜色(代表寄存器)为节点着色,相邻节点颜色不同。若颜色数超过可用寄存器数(如16个XMM寄存器),则需将部分变量“溢出”(Spill)到内存栈。
具体到本例:
y是局部变量,通常分配在栈上,地址如-4(%rbp);- 编译器需先将
y从内存加载到寄存器:movss -4(%rbp), %xmm0; - 将常量
2.0加载到另一寄存器(或直接编码):movss .LC0(%rip), %xmm1(.LC0存放2.0); - 执行乘法:
mulss %xmm1, %xmm0; - 将结果存回
t1(可能也是栈位置):movss %xmm0, -8(%rbp)。
这个过程凸显了目标代码生成的约束:指令集架构(ISA)的物理限制。x86-64有16个XMM寄存器,但并非所有指令都能用所有寄存器;ARM64有32个V寄存器,但浮点指令编码方式不同。考试虽不考具体汇编,但会考“以下哪种情况会导致寄存器溢出?”,答案必然是“活跃变量数超过可用寄存器数”。
注意:目标代码生成还必须处理调用约定(Calling Convention)。例如,x86-64 System V ABI规定,前6个整型参数用
%rdi,%rsi,%rdx,%rcx,%r8,%r9传递,浮点参数用%xmm0-%xmm7。main函数的返回值0,必须放入%eax。这些规则是代码能正确运行的基石,也是调试时查看寄存器值的关键依据。
4. 实操过程与核心环节实现:手把手复现一次微型编译流程
4.1 准备工作:搭建极简环境,5分钟完成工具链初始化
本实操不依赖复杂IDE,仅需基础命令行工具,确保零门槛。所有操作均在Ubuntu 22.04(或WSL2)下验证,macOS用户可替换apt为brew,Windows用户推荐WSL2。
步骤1:安装JFLAP(Web版,免安装)
- 打开浏览器,访问官方JFLAP网站(jflap.org),点击“Run JFLAP in Browser”;
- 无需下载,直接在浏览器中启动Java Web Start(现代浏览器可能提示安全警告,选择“允许”或“保留”即可);
- 首次运行稍慢,耐心等待Java Applet加载完成。
步骤2:获取TINY Compiler源码
- 打开终端,执行:
wget https://www.cs.rochester.edu/~scott/tiger/code/tiny.tar.gz tar -xzf tiny.tar.gz cd tiny make make命令会编译出tiny可执行文件。若提示缺少yacc/lex,执行sudo apt install yacc lex即可。- 验证:
./tiny -h应显示帮助信息。
步骤3:创建测试用例test.c
- 在
tiny目录同级新建文件test.c,内容即为前文的main函数示例。注意:TINY是教学语言,语法略有简化,我们使用其标准格式:/* test.c */ int main() { int x; float y; x = 10; y = 3.14; if (x > 5) { y = y * 2.0; } return 0; }
为什么选这个环境?因为JFLAP和TINY是经过数十年教学验证的“认知脚手架”。它们代码量小(TINY核心仅500行)、注释全、错误信息友好,且完全开源。相比动辄数万行的GCC或Clang,它们让你一眼看穿编译器的“心脏”。我坚持用它们,是因为在辅导中发现:当学员亲手用JFLAP画出一个状态机,并成功识别出123abc是非法标识符时,那种“啊哈!”的顿悟感,是任何PPT都无法替代的。
4.2 实操1:用JFLAP构建C语言标识符识别器,亲手验证最长匹配原则
现在,我们动手构建一个能识别C语言标识符的词法分析器状态机。打开JFLAP,选择File → New → Finite Automaton。
步骤1:定义状态与转移
- 创建状态:
q0(初始状态)、q1(接收字母/下划线后的状态)、q2(终态,标识符结束); - 添加转移:
q0--a-z, A-Z, _-->q1(接收首字符);q1--a-z, A-Z, 0-9, _-->q1(接收后续字符);q1--blank, ;, (, ), {, }, +, -, *, /, =, >, <, !, ,-->q2(遇到分隔符,进入终态)。
步骤2:测试与验证
- 点击
Input → Multiple Run,输入测试字符串:myVar:应接受(路径q0->q1->q1->q1->q2);123var:应拒绝(q0无法接收1,无转移);my-var:应拒绝(-不在q1的转移字符集中,卡在q1)。
关键观察:当输入==时,我们的状态机并不处理它,因为==是运算符,不属于标识符范畴。这正说明词法分析是“分治”的:一个专用的状态机处理标识符,另一个处理运算符。考试中若问“词法分析器如何区分==和=”,答案