简介:这是一份《编译原理课程设计》报告书,围绕C语言词法分析器和C-语言语法分析器的设计与实现展开。文档从实验目的与意义切入,详细梳理C语言保留字、符号、标识符等词法特点,给出正则表达式定义、Token类型及类型代码,并结合DFA状态转换图讲解词法分析器的工作流程。内容包含注释DFA与词法分析DFA等状态图,可帮助读者直观理解自动机如何识别数字、标识符、字符串和运算符;同时说明语法分析器的工作原理,适合计算机专业学生完成编译原理课程设计、撰写实验报告或复习相关考点时参考。资源包为单个doc文档,大小约379KB,目录结构完整,兼具报告模板与实现思路参考价值;已有274人学习下载,对想快速理清DFA构造、Token分类和语法分析整体框架的学习者尤为实用。
1. 从一个答辨现场翻车说起
刚拿到“C语言词法分析器和C语言语法分析器编译原理课程设计报告书”这个题目时,多数人第一反应是去网上找一份现成的代码改一改,但我见过太多人代码跑起来了、报告却答不上来。老师指着状态转移图问“为什么这个状态遇到字母会跳到标识符状态”,你不说清楚,报告写得再厚也白搭。这篇笔记就是为准备做这个课程设计的你写的——我把词法分析器和语法分析器的最小可运行实现、报告书该怎么组织、以及最容易让程序“看着能跑、答辩就翻车”的坑都摊开讲。前半部分适合新手照步骤搭,后半部分适合熟手核对设计边界。核心目标很简单:让代码和报告书互相对得上,答辨时心里有底。
2. 词法分析器:把字符流切成 token 才是编译的第一关
2.1 为什么要手写状态机:正则表达式只是思路,不是交付物
词法分析器要解决的第一个问题,是把 C 语言源代码这个超长字符串切成有意义的“单词”。int a = 42;在编译前端眼里不是五个字符,而是五个 token:关键字int、标识符a、赋值运算符=、数字常量42、分号;。每个 token 至少要带两类信息:类别是什么、原始文本是什么。常见做法是维护一个TokenType枚举加一个字符缓冲区,类别决定后续语法分析怎么消费,文本用于报错和生成符号表。
你完全可以靠正则表达式把规则写出来,然后用 flex 自动生成分析器,但课程设计的评估重点往往不是“能不能跑”,而是“有没有把状态转移的逻辑说清楚”。手写词法分析器通常意味着用一个有限自动机(DFA)来扫描字符流:每读一个字符就依据当前状态决定下一个状态,状态标到某个“接受态”时输出一个 token。这样一个switch (state)或者二维状态转移表,就能画成老师最爱看的状态转移图,也方便你在答辩时直接指出“这个分支对应图上的哪条弧”。
C 语言词法上的坑其实很固定:空白字符要跳过但必须记录换行;标识符能包含数字但不能以数字开头;关键字本质上是“已经被预留的标识符”;数字常量不能写成12abc;运算符有+、++、+=这种最长匹配问题。手写状态机时,我会把字符先分类再喂给状态迁移逻辑,而不是每个状态堆几十个if,否则代码会膨胀到难维护。字符分类可以简单定义为:字母或下划线、数字、空白、运算符、界符。这样状态转移表的规模可以被压得很小。
2.2 用状态转移表实现一个能跑的最小词法分析器
下面这段代码是我常用的最小骨架,它只覆盖标识符、数字和几个运算符,但结构是完整的:读一个字符,按当前状态决定迁移,到达接受态后回退一个字符再返回 token。这里的“回退一个字符”很关键,因为很多 token 的结尾要靠读到不属于它的字符才能判断出来,不把那个字符放回去就会丢字符。
#include <stdio.h> #include <string.h> #include <ctype.h> #define TOKEN_MAX 128 typedef enum { T_EOF, T_ID, T_NUM, T_ASSIGN, T_ADD, T_SUB, T_MUL, T_DIV, T_LP, T_RP } TokenType; typedef struct { TokenType type; char text[TOKEN_MAX]; int line; } Token; typedef enum { ST_START, ST_ID, ST_NUM, ST_DONE } State; static Token make_token(TokenType t, const char *text, int line) { Token tk; tk.type = t; tk.line = line; strncpy(tk.text, text, TOKEN_MAX - 1); tk.text[TOKEN_MAX - 1] = '\0'; return tk; } Token next_token(FILE *fp, int *line) { int ch; char buf[TOKEN_MAX]; int len = 0; State st = ST_START; while ((ch = fgetc(fp)) != EOF) { switch (st) { case ST_START: if (isspace(ch)) { if (ch == '\n') (*line)++; continue; } if (isalpha(ch) || ch == '_') { buf[len++] = (char)ch; st = ST_ID; } else if (isdigit(ch)) { buf[len++] = (char)ch; st = ST_NUM; } else { switch (ch) { case '=': return make_token(T_ASSIGN, "=", *line); case '+': return make_token(T_ADD, "+", *line); case '-': return make_token(T_SUB, "-", *line); case '*': return make_token(T_MUL, "*", *line); case '/': return make_token(T_DIV, "/", *line); case '(': return make_token(T_LP, "(", *line); case ')': return make_token(T_RP, ")", *line); default: fprintf(stderr, "无法识别的字符: %c\n", (char)ch); return make_token(T_EOF, "", *line); } } break; case ST_ID: if (isalnum(ch) || ch == '_') { if (len < TOKEN_MAX - 1) buf[len++] = (char)ch; } else { ungetc(ch, fp); st = ST_DONE; } break; case ST_NUM: if (isdigit(ch)) { if (len < TOKEN_MAX - 1) buf[len++] = (char)ch; } else { ungetc(ch, fp); st = ST_DONE; } break; default: break; } if (st == ST_DONE) break; } buf[len] = '\0'; if (st == ST_ID) return make_token(T_ID, buf, *line); if (st == ST_NUM) return make_token(T_NUM, buf, *line); return make_token(T_EOF, "", *line); }这段代码的逻辑说明:ST_START是初始状态,遇到空白就跳过并统计换行;遇到字母或下划线跳入ST_ID,遇到数字跳入ST_NUM,遇到运算符直接返回单字符 token。ST_ID状态下持续吸收字母、数字、下划线,直到读到一个不属于标识符的字符,这时用ungetc把它退回输入流,再标记ST_DONE。ST_NUM同理,只吸收数字。最后ST_DONE触发循环结束,返回 token。
这个代码的参数和边界有几个要留意的地方:TOKEN_MAX是缓冲区上限,防止超长标识符溢出,但这里只是静默截断,实际课程设计里最好在截断时报错;ungetc只能回退一个字符,所以不能把已经读入的多个字符倒回去;line用指针传进来,是因为 token 结构体里要记录行号,后续报错时能指出位置。要支持关键字也很简单,ST_ID吸收完标识符后查一张关键字表,如果命中就把类型改成对应的关键字类型。常见的做法是建立一个const char *keywords[]数组,用strcmp线性查,量不大时性能完全够。
2.3 词法分析报告书的核心素材:token 类别表、状态转移图和测试输出
报告书的词法部分不是让你堆代码,而是要让老师一眼看到“你设计了哪些 token、状态怎么流转、测试覆盖了什么”。我会在报告里放三样东西:token 类别表、状态转移图、一组带截图的测试用例。token 类别表用表格列得非常具体。
| 类别 | 类型枚举 | 样例 | 说明 |
|---|---|---|---|
| 关键字 | T_IF, T_ELSE, T_INT 等 | if, else, int | 在标识符识别后查表得到 |
| 标识符 | T_ID | foo, _x, a1 | 字母或下划线开头,后跟字母/数字/下划线 |
| 数字常量 | T_NUM | 0, 42, 007 | 至少一位数字 |
| 赋值运算符 | T_ASSIGN | = | 单字符运算符 |
| 算术运算符 | T_ADD, T_SUB, T_MUL, T_DIV | + - * / | 单字符运算符 |
| 界符 | T_LP, T_RP | ( ) | 单字符界符 |
状态转移图可以手画,也可以用 Graphviz 的 dot 语法生成,但不建议在报告里贴太复杂的图。只要画出ST_START -> ST_ID -> ST_DONE和ST_START -> ST_NUM -> ST_DONE这两条主路径,再加上空白跳过和错误状态即可。老师问得最多的反而是“如果输入是 123abc,你的分析器怎么处理”,因为很多实现会把数字吸收完再遇到字母时报错,或者直接输出两个 token,这个边界你要先想清楚,然后在报告里写“123abc 属于非法词法单元,遇到数字后紧跟字母应报错”。
测试输出建议用真实命令行截图,不要贴那种在 IDE 里跑出来的杂窗口。我一般会准备三段输入:一段是普通表达式,一段是含空行、制表符和注释(如果有)的代码,一段是故意出错的123abc。词法分析器只需要把每个 token 的类型和文本打印出来,例如T_ID: a、T_NUM: 123,截图放在报告里就能验证工作量。
3. 语法分析器:把 token 流按要求组合成语法结构,递归下降最稳
3.1 文法设计:先消除左递归,再写分析函数
词法分析器把int a = 42;切成了六个 token,语法分析器要做的是判断这个序列是否符合 C 语言的语法规则,并且构建出一棵语法树。课程设计里最常见的实现是递归下降分析,因为它思路直白、代码量可控,而且报告书里可以画 LL(1) 分析表来配合说明。如果选 LR 类分析工具,报告书会显得像黑匣子,答辨时反而难讲清楚。
递归下降要求文法最好是 LL(1) 的,也就是每个非终结符在展开时要能根据当前 token 唯一决定走哪个产生式。因此第一步是先定义一个小型 C 语言子集文法,然后消除左递归。下面这个文法是我常用的版本,它覆盖赋值语句、表达式、括号和四则运算,足够支撑一个课程设计演示。
program := stmt_list stmt_list := stmt stmt_list | ε stmt := assign_stmt assign_stmt:= id '=' expr ';' expr := term expr_tail expr_tail := '+' term expr_tail | '-' term expr_tail | ε term := factor term_tail term_tail := '*' factor term_tail | '/' factor term_tail | ε factor := id | num | '(' expr ')'注意这里的expr_tail和term_tail都是从右递归描述的,因为expr产生式已经改写了,原始的左递归形式expr := expr + term会让递归下降掉进无限循环。为什么左侧递归会死循环?因为parse_expr()函数进去第一件事还要调parse_expr(),token 一个都没消费,栈很快就溢出了。所以“消除左递归”不是可有可无的包装,而是必须落实的代码约束。
报告书里写文法时,最好用产生式列表加一段说明的方式,不要只贴一个 ebnf 文件。我会为每个非终结符标出 First 集合,因为 First 集合决定了递归下降函数用什么 token 作为入口判断。例如expr的 First 是{ id, num, '(' },那么在parse_stmt里看到这三种 token 之一就知道该进入表达式解析。Follow 集合则用来处理空产生式,比如expr_tail遇到;或)时要停止继续吃运算符。
3.2 递归下降核心代码:能检查赋值语句并打印错误位置的最小实现
下面这段代码实现了对赋值语句和表达式的语法检查。它没有真正生成语法树节点,而是把关注点放在“能不能匹配成功、出错位置在哪”,这是课程设计最稳的起点。你可以在它的基础上扩展 if/else 或 while,也可以把每个函数改成返回节点指针来构造 AST。
#include <stdio.h> #include <stdlib.h> typedef enum { T_EOF, T_ID, T_NUM, T_ASSIGN, T_ADD, T_SUB, T_MUL, T_DIV, T_LP, T_RP, T_SEMI } TokenType; typedef struct { TokenType type; char text[128]; int line; } Token; static Token current; static int parse_error = 0; static void next_token(void) { // 这里替换成上一章实现的 next_token,并从全局 FILE* 中读取 } static void error(const char *msg) { if (!parse_error) { fprintf(stderr, "第 %d 行 语法错误: %s\n", current.line, msg); parse_error = 1; } } static int match(TokenType t) { if (current.type == t) { next_token(); return 1; } return 0; } static void parse_factor(void) { if (current.type == T_ID || current.type == T_NUM) { next_token(); } else if (current.type == T_LP) { next_token(); parse_expr(); if (!match(T_RP)) error("缺少右括号"); } else { error("表达式里出现了意外的 token"); } } static void parse_term_tail(void) { if (current.type == T_MUL || current.type == T_DIV) { next_token(); parse_factor(); parse_term_tail(); } } static void parse_term(void) { parse_factor(); parse_term_tail(); } static void parse_expr_tail(void) { if (current.type == T_ADD || current.type == T_SUB) { next_token(); parse_term(); parse_expr_tail(); } } static void parse_expr(void) { parse_term(); parse_expr_tail(); } static void parse_assign_stmt(void) { if (current.type == T_ID) { next_token(); if (match(T_ASSIGN)) { parse_expr(); if (!match(T_SEMI)) error("赋值语句缺少分号"); } else { error("标识符后面缺少赋值符号"); } } else { error("语句必须以标识符开头"); } } static void parse_program(void) { while (current.type != T_EOF) { parse_assign_stmt(); } }这段代码的函数调用链对应了文法:parse_program循环消费语句;parse_assign_stmt先看T_ID,再吃赋值号和表达式;parse_expr调parse_term再调parse_expr_tail;parse_expr_tail遇到加号或减号就再吃一个parse_term并递归。parse_error是个保险开关,一旦出错就不再叠加报错,避免输出几十条噪声。老师追问“为什么没有考虑优先级”时,你就可以回答:expr_tail只处理加减、term_tail只处理乘除,天然保证了乘除先于加减。
这个实现里最容易忽视的是“没有 token 消耗的死循环”。如果把expr_tail写成if (current.type == T_ADD) parse_term();而忘了递归调用expr_tail,那么在遇到连续两个加号时就会漏分析。另一个隐藏问题是next_token的实现要跳过空格和注释,否则current会停留在空白 token 上。常见做法是词法分析器遇到空白直接跳过,不在 token 流里产生空白类型,这样语法分析器就不需要处理。
3.3 报告书中的 First 和 Follow 集合:手算的过程比结论更值钱
语法分析部分的报告书不能只放代码,至少要有一节叫“预测分析表的构造”,里面写出几个关键非终结符的 First 和 Follow 集合。计算过程其实不复杂,但很多同学喜欢直接抄网上的结论,导致和代码不一致。比如上面这个文法,expr的 First 集合来自term,而term的 First 集合来自factor,所以First(expr) = First(term) = { id, num, '(' }。expr_tail的 First 是{ '+', '-' },但因为存在ε产生式,Follow(expr_tail) 还需要看谁能在它后面出现。
| 非终结符 | First 集合 | Follow 集合 |
|---|---|---|
| program | { id } | { EOF } |
| assign_stmt | { id } | { EOF, id } |
| expr | { id, num, ( } | { ;, ) } |
| expr_tail | { +, - } | { ;, ) } |
| term | { id, num, ( } | { +, -, ;, ) } |
| term_tail | { *, / } | { +, -, ;, ) } |
| factor | { id, num, ( } | { *, /, +, -, ;, ) } |
报告书里写这张表的时候,我会附一句“手算方式是:非终结符产生式右部首符号如果是终结符就收入 First,如果是非终结符就递归求其 First;Follow 则从开始符号加$,然后扫描所有产生式,把某个非终结符后面的符号收入跟随集合”。不要小看这句话,答辨时老师很可能让你现场算一个。计算完后,还可以画出一个简化的 LL(1) 预测分析表,横轴是终结符,纵轴是非终结符,表里填产生式编号,这样就和递归下降函数一一对应。
4. 课程设计报告书:把代码过程变成老师看得懂的逻辑链
4.1 报告书的标准结构不是流水账,而是设计证据链
这门课程的报告书一般要求 Word 文档,标题里那份“报告书.doc”通常包含封面、摘要、需求分析、总体设计、详细设计、测试结果和总结。很多同学把“详细设计”写成大段源代码,这是报告书最常犯的错误。老师的阅读预期不是 read the code,而是“你遇到了什么问题、用什么方案解决、最终效果如何”。所以每个章节都应该服务于一条逻辑链:输入是什么、输出是什么、中间经历了哪些阶段。
我写报告书时会在“需求分析”一节先明确输入输出格式。例如输入是任意一个.c文件,输出是两份结果:词法分析输出 token 列表,语法分析输出“语法正确”或错误行号。这里不要用含糊的“支持 C 语言大部分语法”,要精确说“支持变量赋值、表达式、变量声明、if/else(如果做了)”。把范围写小一点没有坏处,老师看到你能自觉限定子集,反而会觉得你有工程意识。
总体设计一节建议画一张分层的模块图:输入源程序 → 词法分析器 → token 流 → 语法分析器 → 分析结果。这个图用 Word 里的文本框画就行,不需要炫技。每一层之间用箭头标出传递的数据结构,比如“Token 数组”或“出声生成器回调”。课程设计的规模不大,一张图就能讲清楚架构。
详细设计是最厚的一章,我习惯拆成“词法分析设计与实现”“语法分析设计与实现”“错误处理设计与实现”三小节。每小节先写数据结构定义,再写算法步骤,最后贴关键代码片段。代码不要整段贴,只贴核心状态转换或递归调用链,并紧跟一段文字说明为什么这样写。比如贴ST_ID分支时,说明“这里用 ungetc 回退字符,避免丢 token”。这种细节比代码块本身更让老师放心。
4.2 测试用例怎么设计才能覆盖“正常、边界、错误”三类
报告书的测试部分是最容易凑字数的,但也是老师判断你有没有真正运行过的地方。我会设计一个测试用例表格,每条用例包含“输入片段、预期输出、实际输出、是否通过”。正常用例是像a = 1 + 2 * (3 - 4);这样的普通表达式;边界用例包括连续多个空格、空行、以关键字开头的标识符(比如int2 = 3;,其中int2应该是标识符而不是关键字);错误用例包括a = b;缺少分号、a = (b;缺少右括号、123abc词法错误。
这些用例不要只放在报告里,还要在验收现场跑一遍。我通常准备一个test_cases/目录,每个文件只装一个用例,然后写一个批处理脚本逐个调用分析器,把输出重定向到results/目录。报告书里放一个汇总表就行,具体运行截图放两张:一张全对过,一张故意报错。报错截图反而更显真实,因为没有任何分析器能处理所有非法输入。
这里还有一个细节:输出格式要稳定。词法分析器每行输出(<行号>, <类型>, <文本>),语法分析器出错时输出Error at line N: ...。这样测试用例表格里可以方便地贴预期输出。不要用 IDE 自带的可视化界面做演示,因为答辩电脑上的调试环境不一定一样。命令行输出最保险。
4.3 报告里的图与截图:状态转移图、语法树、调试痕迹
状态转移图和语法树是这门课程设计最强的视觉证据。我建议至少画两幅图:词法分析器的状态转移图和a = 1 + 2;对应的语法分析树。状态转移图可以手工画节点和箭头,但要保证和代码分支一致。比如代码里有ST_START、ST_ID、ST_NUM三个状态,图里就不能多画一个ST_STRING。老师很喜欢在图上找一个代码里不存在的状态,这是送分题也是送命题。
语法分析树的画法更简单,把产生式展开过程画成树形结构即可。比如表达式1 + 2 * 3的树根是expr,左子树term -> factor -> 1,右子树expr_tail -> + term -> factor 2嵌套term_tail -> * factor 3。如果你在代码里实现了抽象语法树节点,截图打印节点时可以用括号表示法(expr (term (factor 1)) (expr_tail + (term ...))),报告里贴这个输出,不会再被质疑“你到底建树没有”。
调试截图方面,我用得最多的不是断点截图,而是给词法分析器加一个-debug参数,用它打印 token 流和语法分析栈。比如运行./parser -debug test.c时输出:
token: T_ID text=a token: T_ASSIGN text= token: T_NUM text=1 ... parse assign_stmt at line 1 parse expr at line 1这种输出比内存视图更直白,也更容易在报告书里排版。如果你要用 GDB 调栈,也可以截一张bt命令的图,但要有注释解释清楚调用栈对应哪些递归函数。报告书里不要放没有说明的截图,每张图下面写“这张图说明了什么”。
5. 避坑指南:词法语法分析器课程设计最常见的五个坑
5.1 现象:关键字和标识符串门
输入int main时,词法分析器把int输出成T_ID,导致语法分析器把声明语句当成赋值语句处理,报错位置莫名其妙。原因通常是关键字表匹配时机错了。如果先判断“以字母开头就返回标识符”,那么int永远来不及查关键字表。解决方法是先把整个标识符吸收进缓冲区,闭包后再查关键字表。也就是说,所有以字母或下划线开头的字符都走同一条ST_ID状态,吸收完后再统一分类,是关键字就返回关键字类型,否则返回T_ID。这条规则在报告书里用一句话写清楚,就能避免答辩时被指出设计缺陷。
5.2 现象:递归下降遇到表达式就死循环
很多同学写完parse_expr后,一运行就卡死或者栈溢出。原因几乎都是文法没有消除左递归。expr := expr + term看起来很美,但代码里parse_expr()第一行又调用parse_expr(),token 一个没吃,编译器只会给我们一个 segmentation fault。解决方法是把左递归改写为右递归,也就是前面那套expr_tail方案。另外还要检查每个非终结符函数是否至少消费了一个 token;如果没有,进入下一层递归前就要判断当前 token 是否能匹配产生式的开头,否则即使是右递归也可能互相空转。
5.3 现象:报告中的 First/Follow 表和代码不一致
报告书上写First(expr) = { id },但代码里明明允许表达式以(开头,答辩时老师翻到这一页就把你温柔地敲打一顿。原因通常是先写了代码,再倒推手算集合时算错或抄了别人的表。解决办法是在代码里写一个调试函数,根据已实现的产生式现场输出 First 集合,然后把输出截图作为报告书的依据。至少要在报告里写清楚“表中只列出关键非终结符,完整结果见附录”,不要硬撑一个看起来完整的表。
5.4 现象:遇到注释或预编译指令直接乱掉
C 语言源代码里几乎都会有//、/* ... */和#include。如果词法分析器没有处理注释,遇到//时会把第二个/误解为除号,然后再把整行注释内容当成一堆未知 token,语法分析必然翻车。解决方法是给状态机加两个注释状态:ST_LINE_COMMENT和ST_BLOCK_COMMENT。遇到/时偷看一眼下一个字符,如果是/就进入行注释状态,读到换行返回ST_START;如果是*就进入块注释状态,直到遇到*/。#include这类预编译指令不在大多数课程设计范围内,最简单的处理是在需求分析里声明“只支持普通函数体内的语句,不支持预处理指令”,但这会让测试输入受限。建议至少在词法分析器里把#开头的一整行跳过,这样你就可以拿简单的测试文件而不至于崩掉。
5.5 现象:token 缓冲区越界导致报错位置不准
当源代码里出现一个超长标识符,比如连续 200 个字符没有空格,缓冲区buf[TOKEN_MAX]会越界。现象不是立刻崩溃,而是后续 token 文本乱掉,最后语法分析器在错误的位置报错。原因很简单:写缓冲区时没有检查长度,len超过了TOKEN_MAX。解决方法是像前面代码那样,在每次写buf[len++]之前判断len < TOKEN_MAX - 1,并在越界时打印错误“标识符长度超过上限”。不要忽略这种看似边缘的输入,老师最爱拿一个 200 字符的变量名来测试你的边界意识。其实这背后就是 C 语言基本功里的数组和指针边界问题,你可以在报告书的“错误处理”一节专门提,属于加分项。
6. 进阶验证:在答辩现场让程序“说清楚”自己做了什么
6.1 用 GDB 调出 token 流,打给老师看
如果你已经把代码跑通,最推荐的验收前做法是给程序加一个调试开关,把词法分析和语法分析的内部过程打印出来。不要临时在答辩时开 GDB 单步,那样太慢,而且老师可能不耐烦。我习惯的做法是在main函数里用环境变量或命令行参数控制输出级别:传-v时打印每个 token,传-p时打印语法分析进入和退出的非终结符名。这样你只要提前跑一遍,把-v输出保存下来,报告书和 PPT 里都能用同一份数据。如果老师现场问“你这个 token 流里面为什么没有空格类型”,你就打开命令行当场跑给他看,答案自然就有了。
6.2 扩展一个小目标:加上 if/else 的作用域判断
完成基础赋值语句后,再花半天给语法分析器加一个if语句,课程设计档次会明显不一样。文法只需要加stmt := if '(' expr ')' stmt,递归下降函数则是在parse_stmt里先判断current.type == T_IF,然后吃掉(、解析表达式、吃掉),再递归调用parse_stmt()。这里最大的坑是else的悬挂二义性,你可以直接规定“每个if都必须有else”,或者实现“就近匹配”。报告书里把这个问题写出来,本身就是很好的讨论素材。变量作用域可以先不实现,但可以在符号表里预留一个保存变量类型的字段,词法分析器遇到int、float这些关键字时记录当前声明的变量名,供语法分析器做重复声明检查。
6.3 验收前十分钟:把报告书里的图与代码逐行对齐
最后一次检查时,我会把报告书里的每一张状态转移图、每一条 First/Follow 记录和代码里的对应函数列一张对照表。图上有ST_ID状态,代码里就必须有对应的case ST_ID:;报告里写Follow(expr_tail) = { ;, ) },代码里就必须保证parse_expr_tail遇到分号和右括号时能够正常返回而不是报错。以前我总把报告书放在最后写,结果图和代码对不上,答辩前熬夜改,非常痛苦。后来我改成先写测试用例,再实现代码,最后填报告,返工少很多。这个顺序你如果还没开始,很值得试一试。希望帮到你。
本文还有配套的精品资源,点击获取