新闻详情

新闻详情

首页 / 资讯中心 / 详情

手写DFA词法分析与递归下降语法分析:编译原理课程设计实战

发布时间:2026/10/1 9:48:05来源:尧图网络
手写DFA词法分析与递归下降语法分析:编译原理课程设计实战
简介面向编译原理课程设计与实验场景完整覆盖词法分析、基于LL(1)文法的语法分析可解析ii*i等简单表达式、LR(0)与SLR(1)语法分析并延伸到四元式生成与汇编代码生成构建了一套小型编译器全流程源码。资源共14个文件以4个cpp和3个c源程序实现分析器与编译器核心逻辑配合3个h头文件界定模块接口3个txt存储文法规则说明另有1份doc格式的课程设计报告压缩包整体仅557KB体量轻便适合快速部署学习。已有2347人学习下载。除可直接编译运行的词法分析器、LL(1)分析器、LR(0)/SLR(1)分析器外还包含多种LL(1)文法与一个SLR(1)文法定义、四元式生成与汇编生成代码配套报告有助于理解源程序到中间代码再到目标代码的完整链路适合作为编译原理课程设计参考及实验排错对照。1. 只靠一个能跑的词法分析器过不了编译原理课程设计我在验收现场看到翻车最多的一类学生把词法分析器单独调了两周语法分析只剩半天硬凑最后整个小组都在查“为什么a[3]里的[被当成普通运算符”。编译原理课程设计要的从来不是单个组件跑通而是词法分析、语法分析、小型编译器这条链路对上同一份源码能彼此咬合。它解决的核心问题是当你定义了一个小型 C 语言子集每个字符从哪来、归到哪、下一步往哪走失败时在哪一行报错。这篇文章面向正在做课设、或者想用一个周末搭出可演示编译器的读者我会按“手写 DFA → 递归下降 → 三地址码 → 实验报告”的顺序讲所有代码都能直接改成你自己的语言子集。2. 词法分析用手写 DFA状态表怎么画、token 怎么回退词法分析最容易被低估。很多教程让你直接调正则库一个re.findall就把标识符和数字捞出来看起来省事但课程设计里这就是埋雷正则库的错误位置往往只有“无法匹配”这一条信息而你需要的是“第 3 行第 8 列出现非法字符”这种可定位输出。再者语法分析器每次只向前看一个 token词法层如果不把空白、注释、非法字符全部消化掉语法层就会反复被空行和缩进打断。常见做法是手写一个状态迁移表驱动的小型 DFA代码量不大却能让后面每一步都可控。2.1 为什么不建议直接用正则表达式库先说实话用正则写“识别整数或浮点数”确实能过单测像[0-9](\.[0-9])?一眼就能看懂。但课程设计给的源文件不会是干巴巴一个数字而是带缩进、带注释、带连续空行的程序。正则库匹配失败时不会告诉你具体位置也不会告诉你“这个字符已经读进来了现在需要回退一个字符”。这个东西叫输入流的回退正是写正则时最不需要关心、写手写分析器时最需要关心的点。手写 DFA 的好处是每进入一个状态你都知道下一个字符该往哪走。越界时你手里有当前字符和当前状态可以拼出错误信息。更重要的是token 结束时的回退操作只回退一个字符逻辑很直接不会出现正则库在底层帮你缓存一堆状态导致调试困难的情况。对课程设计这个规模来说手写 50 行状态迁移比引一个正则引擎更划算。2.2 token 的状态表数字、标识符、小数点和回退状态设计不用复杂。只需要覆盖课程设计语言子集里最常见的几类词法单元整数、浮点数、标识符/关键字、运算符、分隔符。下面这张表按“当前状态 读入字符 → 下一状态”画状态 0 是起始状态。当前状态读入字符下一状态动作0 开始数字1累积0字母或 _3累积0.5累积等数字0空白0跳过并记录行号1 整数数字1累积1.5累积开始小数部分1其它—回退一个字符返回整数5 小数起点数字2累积5其它—回退并报“小数点后缺数字”2 浮点数数字2累积2其它—回退返回浮点数3 标识符字母、数字、_3累积3其它—回退返回标识符或关键字“回退”指的是ungetc把一个字符放回输入流。这个动作只对单个字符可靠所以状态表里不要出现“回退两个字符再处理”的设计。C 标准里ungetc保证至少能回退一个字符连续回退多个在某些编译器上会失效课程设计阶段按一次只回退一个写最稳。状态转移写成代码就是一个switch。下面的例子可以当词法分析器的骨架#include stdio.h #include ctype.h #include string.h #define MAX_ID_LEN 64 #define MAX_SYMBOLS 1024 static FILE *src; typedef enum { TOK_INT, TOK_FLOAT, TOK_ID, TOK_KEYWORD, TOK_OP, TOK_END, TOK_ERR } TokenType; typedef struct { TokenType type; char lexeme[MAX_ID_LEN]; int line; } Token; static int dfa(int state, char c) { switch (state) { case 0: if (isdigit(c)) return 1; if (c .) return 5; if (isalpha(c) || c _) return 3; return -1; case 1: if (isdigit(c)) return 1; if (c .) return 5; return -1; case 5: if (isdigit(c)) return 2; return -1; case 2: if (isdigit(c)) return 2; return -1; case 3: if (isalnum(c) || c _) return 3; return -1; default: return -1; } } static int is_keyword(const char *s) { static const char *keywords[] { int, float, if, else, return, NULL }; for (int i 0; keywords[i]; i) { if (strcmp(s, keywords[i]) 0) return 1; } return 0; } Token next_token(void) { Token tok {0}; int state 0, len 0, c; while ((c fgetc(src)) ! EOF) { if (state 0 isspace(c)) { if (c \n) tok.line; continue; } if (state 0 strchr(-*/!;,(){}[], c)) { tok.type TOK_OP; tok.lexeme[0] (char)c; tok.lexeme[1] \0; return tok; } int ns dfa(state, c); if (ns 0) { state ns; if (len MAX_ID_LEN - 1) { tok.lexeme[len] (char)c; tok.lexeme[len] \0; } else { tok.type TOK_ERR; snprintf(tok.lexeme, MAX_ID_LEN, token 长度超过 %d, MAX_ID_LEN); return tok; } } else { if (state 0) { tok.type TOK_ERR; snprintf(tok.lexeme, MAX_ID_LEN, 非法字符 %c, c); return tok; } ungetc(c, src); break; } } if (state 0) { tok.type TOK_END; } else if (state 1) { tok.type TOK_INT; } else if (state 2) { tok.type TOK_FLOAT; } else if (state 5) { tok.type TOK_ERR; // 即以 . 开头或 10. 结束的非法数字 } else if (state 3) { tok.type is_keyword(tok.lexeme) ? TOK_KEYWORD : TOK_ID; } return tok; }这段代码里最关键的是第 56 行的ungetc(c, src)。整数10读到分号时分号已经被fgetc取走了不退回的话语法层就永远看不到分号。ungetc之后输入流会重新把分号交给下一次next_token这样词法、语法两个阶段才不会丢字符。tok.line只在状态 0 且遇到换行时自增因为是逐字符读取行号能保持准确后面语法报错时可以直接引用。运算符这里直接作为单字符 token 返回没有进入 DFA 状态。如果课程设计要求支持、这类双字符运算符需要给状态表增加一个“运算符后继状态”在读到第二个运算符字符时再决定返回一个还是两个单字符 token。这里建议先做单字符运算符把主体链路跑通再扩展否则调试时你会分不清问题是出在词法回退还是语法匹配。2.3 符号表先做成线性表别一上来就上哈希课程设计的符号表通常只需要保存三类信息名字、类型、行号。很多同学一上来就写哈希表结果符号还没插进去几个先处理起哈希冲突和扩容反而耽误进度。线性表在这个规模下完全够用代码也更直白。typedef struct { char name[MAX_ID_LEN]; int type; // 0-未定义 1-int 2-float int line; // 第一次出现位置 int used; // 是否被使用过后面做未使用警告 } SymbolEntry; static SymbolEntry symtab[MAX_SYMBOLS]; static int sym_count 0; static int lookup_symbol(const char *name) { for (int i 0; i sym_count; i) { if (strcmp(symtab[i].name, name) 0) return i; } return -1; } static int add_symbol(const char *name, int type, int line) { int idx lookup_symbol(name); if (idx 0) { fprintf(stderr, line %d: 符号 %s 重复定义\n, line, name); return idx; } if (sym_count MAX_SYMBOLS) { fprintf(stderr, 符号表已满MAX_SYMBOLS%d\n, MAX_SYMBOLS); return -1; } strncpy(symtab[sym_count].name, name, MAX_ID_LEN - 1); symtab[sym_count].type type; symtab[sym_count].line line; symtab[sym_count].used 0; return sym_count; }三个参数值得在报告里说明MAX_ID_LEN64是 token 缓冲上限防止一个超长标识符把数组写穿MAX_SYMBOLS1024是符号表容量课程设计里几百个符号顶天了used字段是加分项能让你在后面做“定义了但从未使用”的警告。查表用线性遍历平均几十次比较性能完全不是瓶颈报告里写明这个取舍就行。3. 语法分析选递归下降还是 LR先消除左递归再动手写代码词法分析输出的 token 流进语法分析器后你要回答的核心问题是“这串 token 是不是符合文法的句子”。课程设计最常用递归下降因为代码结构和文法规则一一对应出错时可以精确打印“期望)实际拿到;”。LR 分析器更适合拿 Yacc 这类工具自动生成但表驱动方式一旦冲突排查成本比递归下降高很多。对一个需要在一周内交实验报告的课设来说递归下降是性价比最高的方案。3.1 递归下降和 LR 各自的成本递归下降的直观性在于每个非终结符对应一个函数函数体就是该非终结符的产生式。比如factor : ( expr )直接翻译成“匹配左括号调用 parse_expr匹配右括号”你不必理解状态栈和归约动作。缺点是文法必须满足 LL(1) 条件左递归和公共左因子都要先处理。LR 的优点是可以处理更多文法比如expr : expr term这种左递归不需要改写。但手写 LR 分析表非常反人类课程设计里常用做法是引入 Yacc/Bison 或 PLY让工具生成表和驱动代码。问题是工具生成的报错信息有时很难读懂而且你在实验报告里说不清楚“这个状态转移是怎么冲突的”。我一般的建议是语言子集不超过 30 条产生式坚持递归下降如果老师要求用 LR再上 PLY否则别把时间耗在分析表构造上。3.2 把左递归文法改写成 LL(1)四则运算的例子课程设计最常见的文法就是表达式。自然写法的四则运算是左递归的expr : expr term | expr - term | term term : term * factor | term / factor | factor factor: NUMBER | IDENT | ( expr )左递归会让递归下降函数parse_expr第一次就调用自己永远读不进新 token直接栈溢出或死循环。改写方式是提取左公因子并引入新的非终结符表示尾部expr : term expr expr : term expr | - term expr | ε term : factor term term : * factor term | / factor term | ε factor : NUMBER | IDENT | ( expr )ε在这里表示空产生式。也就是说expr要么消费一个 term继续递归要么什么都不做、直接返回。这个“什么都不做”的分支非常关键它对应表达式读到末尾、即将遇到;或)的场景。计算 FIRST 集合时你只需要知道非终结符FIRST 集合说明exprNUMBER, IDENT, (从 term 看term 从 factor 看expr, -, ε空产生式属于 epsilontermNUMBER, IDENT, (同 expr 的前缀term*, /, ε乘除优先级在这一层FOLLOW 集合里比较重要的是expr的 FOLLOW 包含)和;这决定了expr什么时候选择 ε 产生式。判断方法其实很简单如果下一个 token 不在或-里就认为表达式已经结束。不用把整个 FOLLOW 表背下来但报告里建议写清楚每个非终结符的 FOLLOW这个是评审老师爱看的细节。3.3 递归下降的 C 骨架lookahead 与匹配函数递归下降分析器需要一个当前 token也就是常说的 lookahead。每次匹配成功后把下一个 token 读进来。下面这段代码去掉了符号表处理只保留语法骨架typedef struct { Token cur; int ok; } Parser; static int match_token(Parser *p, TokenType t) { if (p-cur.type t) { p-cur next_token(); return 1; } fprintf(stderr, line %d: 期望 %s实际是 %s\n, p-cur.line, token_name(t), p-cur.lexeme); p-ok 0; return 0; } static int match_op(Parser *p, char op) { if (p-cur.type TOK_OP p-cur.lexeme[0] op) { p-cur next_token(); return 1; } fprintf(stderr, line %d: 期望运算符 %c实际是 %s\n, p-cur.line, op, p-cur.lexeme); p-ok 0; return 0; } static int parse_factor(Parser *p) { if (p-cur.type TOK_INT || p-cur.type TOK_FLOAT) { return match_token(p, p-cur.type); } if (p-cur.type TOK_ID) { return match_token(p, TOK_ID); } if (p-cur.type TOK_OP p-cur.lexeme[0] () { match_op(p, (); if (!parse_expr(p)) return 0; return match_op(p, )); } fprintf(stderr, line %d: factor 处出现非法 token %s\n, p-cur.line, p-cur.lexeme); p-ok 0; return 0; } static int parse_term_tail(Parser *p) { if (p-cur.type TOK_OP (p-cur.lexeme[0] * || p-cur.lexeme[0] /)) { char op p-cur.lexeme[0]; match_op(p, op); if (!parse_factor(p)) return 0; return parse_term_tail(p); } return 1; // ε 产生式 } static int parse_term(Parser *p) { if (!parse_factor(p)) return 0; return parse_term_tail(p); } static int parse_expr_tail(Parser *p) { if (p-cur.type TOK_OP (p-cur.lexeme[0] || p-cur.lexeme[0] -)) { char op p-cur.lexeme[0]; match_op(p, op); if (!parse_term(p)) return 0; return parse_expr_tail(p); } return 1; } static int parse_expr(Parser *p) { if (!parse_term(p)) return 0; return parse_expr_tail(p); }这套代码的递归顺序是expr → term → factor优先级自然形成乘除比加减更靠近 factor所以先被解析。匹配失败时最典型的处理是p-ok 0然后停止整个编译流程。如果想让一个输入里报出多个语法错误需要同步恢复策略也就是跳过 token 直到遇到分号或右花括号这个可以留给进阶。match_token和match_op分开写的原因是同一个 token 类型TOK_OP背后挂着不同运算符直接按类型匹配会吞掉不该吞的符号。比如a (b * c)如果匹配右括号时只判断TOK_OP就可能把后续的乘号当成括号吞进来。按字符匹配更精确虽然多写一个函数但排错省太多时间。4. 小型编译器的中间代码生成三地址码与符号表联动怎么落地语法分析通过不是终点课程设计要的是“小型编译器”也就是至少能输出可读的中间表示。最常用的是三地址码例如a b op c每个指令右侧至多一个运算符。把语法树翻译成三地址码的过程属于语法制导翻译在递归下降函数里每当某个产生式归约完成就生成一条指令。4.1 语义动作挂在哪里parse_term 返回什么设计语义动作前先定一个约定parse_expr、parse_term、parse_factor每个函数都返回一个整数代表操作数的编号。这个编号可以指符号表下标也可以指临时变量号。比如 factor 遇到数字10就把这个常量先放入一个临时变量返回临时变量号遇到标识符a就查符号表返回a的编号。这样上层函数只需要拿到左右两个编号就能拼出t1 a 20.5。static int temp_no 0; static int new_temp(void) { return temp_no; } static void emit3(int dst, int lhs, char op, int rhs) { printf(t%d t%d %c t%d\n, dst, lhs, op, rhs); } // 简化版factor 返回操作数所在的临时号或符号表编号 static int parse_factor_sem(Parser *p, SymbolTable *tab) { if (p-cur.type TOK_INT || p-cur.type TOK_FLOAT) { int tmp new_temp(); printf(t%d %s\n, tmp, p-cur.lexeme); match_token(p, p-cur.type); return tmp; } if (p-cur.type TOK_ID) { int idx lookup_symbol(p-cur.lexeme); if (idx 0) { fprintf(stderr, line %d: 未定义变量 %s\n, p-cur.line, p-cur.lexeme); p-ok 0; return -1; } int tmp new_temp(); printf(t%d %s\n, tmp, p-cur.lexeme); match_token(p, TOK_ID); return tmp; } if (p-cur.type TOK_OP p-cur.lexeme[0] () { match_op(p, (); int val parse_expr_sem(p, tab); match_op(p, )); return val; } p-ok 0; return -1; }这段代码里parse_factor_sem对每个常量或变量都生成一次“加载到临时变量”。实际课程设计里这个做法支持了各种符号引用代价是临时变量会多一些。这不是缺陷反而方便做寄存器分配验证。你在实验报告里可以写“所有左值右值统一走临时变量后续可以在此基础上做常量传播优化”这是明显的加分点。4.2 带语义动作的表达式解析一个示例的完整输出有了 factor 层的语义动作expr 层只需要做组合。下面代码是parse_expr_tail_sem的框架static int parse_expr_tail_sem(Parser *p, int left) { if (p-cur.type TOK_OP (p-cur.lexeme[0] || p-cur.lexeme[0] -)) { char op p-cur.lexeme[0]; match_op(p, op); int right parse_term_sem(p); int tmp new_temp(); emit3(tmp, left, op, right); return parse_expr_tail_sem(p, tmp); } return left; // ε直接把 left 往上传 } static int parse_expr_sem(Parser *p) { int left parse_term_sem(p); return parse_expr_tail_sem(p, left); }对照语法规则expr : term expr对应的语义动作就是“把左边操作数、运算符、右边操作数拼成一条三地址码用新的临时变量号继续递归”。ε产生式没有语义动作只把当前结果返回。这个翻译模式叫“语法制导翻译”代码写起来其实就是把每条printf放在匹配完该产生式全部符号之后。拿int a 10; float b a 20.5;举例生成的中间代码是源代码三地址码输出int a 10a 10float b a 20.5t1 at2 20.5t3 t1 t2b t3这里没有做类型转换处理t1是 intt2是 float按原样输出。如果要更严格需要在float b a 20.5时生成一条t3 (float)t1 t2的强制转换指令。课程设计阶段可以先不做但报告里必须留一个“已知限制”说明否则答辩时被问到会很尴尬。4.3 作用域栈让符号表支持函数块小型编译器如果只支持全局变量符号表用一张线性表就够了。但一旦加了函数或块级作用域重名变量就会互相覆盖。常见做法是引入作用域栈进入函数或花括号时压入一层新作用域退出时弹出。typedef struct ScopeNode { SymbolEntry *symbols; int n; int cap; struct ScopeNode *parent; } ScopeNode; static ScopeNode *current_scope; static ScopeNode *new_scope(ScopeNode *parent) { ScopeNode *s calloc(1, sizeof(ScopeNode)); s-cap 64; s-symbols calloc(s-cap, sizeof(SymbolEntry)); s-parent parent; return s; } static SymbolEntry *lookup_in_scopes(const char *name) { for (ScopeNode *s current_scope; s; s s-parent) { for (int i 0; i s-n; i) { if (strcmp(s-symbols[i].name, name) 0) return s-symbols[i]; } } return NULL; } static SymbolEntry *add_to_current_scope(const char *name, int type, int line) { ScopeNode *s current_scope; if (s-n s-cap) { // 容量不够时翻倍扩容操作和 realloc 一致 s-cap * 2; s-symbols realloc(s-symbols, s-cap * sizeof(SymbolEntry)); } strncpy(s-symbols[s-n].name, name, MAX_ID_LEN - 1); s-symbols[s-n].type type; s-symbols[s-n].line line; s-symbols[s-n].used 0; return s-symbols[s-n]; }作用域规则是查找从当前层往父层找插入只往当前层插。这样内层可以访问外层变量但同名定义不会覆盖外层重定义检测只需查当前作用域是否已经有这个名字。lookup_in_scopes和add_to_current_scope分开的另一个好处是语法分析到赋值语句时可以直接判断“赋值给未声明变量”还是“赋值给内层遮蔽的外层变量”两个错误的报错逻辑不同。5. 编译原理课程设计的 5 个翻车点现象、原因、解决这一章把我和周围人真正踩过的坑按顺序列出来。每一条我都按“现象 → 原因 → 解决”写课程设计做到一半的读者可以直接对照。5.1 打开源文件全是乱码token 里多出“烫烫烫”现象在 Windows 下用 VS 或 Dev-C 打开测试文件中文注释显示乱码词法分析器偶尔把一串“烫烫烫”当成标识符语法层于是报“非法标识符”。原因MSVC 默认按本地代码页 GBK 读源文件而磁盘上的.c文件被存成了 UTF-8。多字节中文注释在按单字节读取时会拆成若干“半个汉字”凑出来的字节正好进入标识符状态。解决统一约定测试文件和工程文件都保存为 UTF-8 without BOM。词法分析器遇到 0xFF 开头的字节直接返回词法错误不要把不可打印字符拼进 token。读文件时不要用fopen默认区域在代码里显式用二进制方式读取把所有非 UTF-8 字符当作非法输入处理。5.2 递归下降一运行就死循环控制台疯狂输出现象输入a 1 2;程序不退出或者直接栈溢出崩溃。原因文法没有消除左递归。parse_expr第一行调用了parse_exprlookahead 根本没被消费函数反复进入下一层递归最终爆栈。解决按第 3 章的方式先把expr : expr term改写成expr : term expr再把expr的递归放在匹配 term之后。一个直观的检查方法看你的 parse 函数有没有可能在没有读取任何 token 的情况下再次调用自己如果可能就是左递归没除干净。5.3 token 逐个看起来都对但语法层总是多出一个空 token现象打印 token 流时a、、1、;都正常但语法分析器每次读完标识符后再看一眼就拿到一个空串 token导致匹配失败。原因空白字符有可能落到了语法层。常见场景是把isspace判断只写在next_token的外层循环里一旦fgetc已经读到了换行符换行被当成 token 的一部分放进lexeme。下次语法层读 lookahead 时拿到的就是一个不可见字符组成的空 token。解决把空白字符的跳过逻辑统一放在状态机的状态 0 中处理且只处理一次。语法层不要自己跳过空白所有可见 token 都由词法层产出。检查方法很简单打印每个 token 的len如果出现len 0或lexeme[0]是\n的 token那就是词法层漏跳了空白。5.4 报错行号永远是第 1 行现象随便写一个有语法错误的文件报错全落在line1。原因行号在fgetc前面累加但分组读取用的fgets会一次取走整行或者词法层在状态非 0 时没有处理换行导致tok.line只有第一次成功加一后面就不再更新。解决像第 2 章的代码那样在状态 0 判断字符时专门处理\n。另一个可靠做法是给next_token传入一个LineContext *每次调用结束时把当前行号写回。不要把行号放在一个临时变量里只在部分分支自增这是最容易出 bug 的位置。5.5 测试用例只测了正常程序答辩时被几个怪输入打崩现象自己写的demo.c能编译老师拿一个“只有注释的空文件”或“连续两个加号”的文件过来程序要么崩溃要么输出一堆错误信息后段错误。原因测试用例没有覆盖空输入、只有注释、非法字符、运算符重复这四类边界。解决准备一组最小测试集按我惯用的分类跑# 1. 合法程序能跑通完整链路 echo int main(){int a105;return a;} ok.c # 2. 语法错误缺少赋值符号 echo int main(){int a 10;} err1.c # 3. 词法错误非法字符 混入 echo int a ; err2.c # 4. 空输入文件只有空白 touch empty.c # 5. 运算符重复a 这种写成两个加号 echo int main(){int a1; a2;} err3.c每次改动词法或语法代码后先跑这五个用例再跑你的正规模样例。这个方法能让大多数边界问题在验收之前暴露出来而不是在老师面前表演段错误。6. 实验报告要抓住的评分点测试样例和错误恢复设计6.1 报告结构从设计目标写到错误恢复别只贴代码实验报告常见的误区是贴大段源码老师看不出你理解了哪个环节。我的建议是报告按五块来组织语言子集定义、总体架构、词法分析设计、语法分析设计、中间代码与测试。语言子集定义放在最前面明确列出支持哪些语句、运算符优先级、关键字和类型这一页就能看出你的工作量。总体架构画一张模块图标明 token 流和符号表在模块之间的传递方向。词法分析部分放状态表、DFA 代码和关键参数。语法分析部分放改写后的产生式、FIRST/FOLLOW 简表和递归下降函数。中间代码部分放三地址码样例以及符号表的作用域规则。最后的测试部分直接把你跑过的合法、非法、边界用例按文件列出来每个文件一行预期结果。这样评审老师能快速判断你的编译器遇到错误输入时是否真的可控。6.2 最容易加分的三个细节第一个是错误恢复策略。能做“报一个错继续找下一个错”的编译器比只报第一个错就中断的明显高一个层次。实现也不复杂在match_token失败后跳过当前 token直到遇到分号或右花括号再恢复分析即可。第二个是未使用变量警告符号表里有一个used字段在赋值或读取时置为 1编译结束后扫一遍给出warning: variable x defined but never used。第三个是行号与列号输出至少行号必须准确这是前面踩坑里最容易翻车也最容易被考评老师看到的点。6.3 验收前按这个顺序自查先把最小合法程序跑通再跑一个带括号的复杂表达式确认优先级正确。然后跑一个未定义变量确认报错带行号。接着跑一个缺少分号的文件确认语法错误能被识别而不是死循环。最后跑空文件和带注释文件确认不会段错误。这个顺序能在 20 分钟内覆盖 90% 的验收场景。我自己做课程设计时养成的习惯是每次改完代码只留一个调试入口把next_token的 token 流打印放到一个开关里调试完毕默认关闭。实验报告里放的是关闭后的干净输出但代码里保留开关答辩时可以现场展示 token 流老师会觉得这个实现是完整的。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

一边承认产品可能毁灭人类,一边要冲击两万亿美元估值 2026/10/1 10:31:41

一边承认产品可能毁灭人类,一边要冲击两万亿美元估值

一边承认产品可能毁灭人类,一边要冲击两万亿美元估值 如果一家公司在招股书里直截了当地告诉投资者:我们做出来的产品可能会敲诈勒索人类、自己对抗关机指令,甚至可能引发人类的生存危机,你觉得这家公司能值多少钱? 常…

阅读更多 →
零收入团队竟换来八十二亿股票,芯片巨头这回彻底不按常理出牌了 2026/10/1 10:31:41

零收入团队竟换来八十二亿股票,芯片巨头这回彻底不按常理出牌了

零收入团队竟换来八十二亿股票,芯片巨头这回彻底不按常理出牌了 一家连年营收几乎为零、团队还不足百人的初创公司,凭什么让一家刚刚跨过万亿美元市值门槛的芯片巨头,一口气掏出价值整整八十二亿美元的真金白银股票? 二〇二六年九…

阅读更多 →
关键字新闻爬虫实战:百度与今日头条数据采集入库全方案 2026/10/1 10:31:41

关键字新闻爬虫实战:百度与今日头条数据采集入库全方案

简介:这是一份基于 Java 实现的新闻爬虫项目,覆盖百度新闻与今日头条两个信源,支持按关键字批量抓取新闻并写入数据库,适合需要采集新闻数据的 Java 开发者、数据分析人员或爬虫初学者参考。压缩包共 20 个文件,包含 1…

阅读更多 →
RuoYi + RAGFlow 私有化知识库工程化实战:权限对齐、检索调优与部署避坑 2026/10/1 10:31:41

RuoYi + RAGFlow 私有化知识库工程化实战:权限对齐、检索调优与部署避坑

1. 从"能跑通"到"敢上线":私有化知识库集成第三阶段到底在解决什么前两篇把 RuoYi 和 RAGFlow 各自跑起来、把接口打通之后,很多人会卡在同一个地方:Demo 里问一句答一句挺顺,一旦把公司几百份制度文件、产品…

阅读更多 →
AMD花82亿美元买世界模型,图什么? 2026/10/1 10:31:41

AMD花82亿美元买世界模型,图什么?

导读: 一家以芯片闻名的公司,拟用约82亿美元的股票买下一家做「世界模型」的公司,还要让李飞飞出任首席科学家。AMD图的是什么?目前能确认的是交易安排和标的方向;技术怎么接入产品、钱怎样赚回来,仍要看后…

阅读更多 →
别再瞎写Java了!这7个代码坏味道赶紧改 2026/10/1 10:31:35

别再瞎写Java了!这7个代码坏味道赶紧改

重复代码:复制粘贴一时爽,维护火葬场同一段逻辑出现在三个以上地方,就是灾难的前奏。今天改需求,你改了A处忘了B处,测试没覆盖到,上线就炸。DRY原则不是口号,是血泪教训。把公共逻辑抽成方法或工…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉