首页/新闻资讯/正文详情

TINY语言词法分析器手写实现:C/C++扫描循环与避坑

发布时间:2026/11/2 11:57:03 来源:云帆数科 栏目:资讯中心
TINY语言词法分析器手写实现:C/C++扫描循环与避坑
简介面向编译原理课程学习与实验的TINY语言词法分析器手工实现工程适合正在完成扫描器tokenizer作业、需要参照 C/C 完整实现的中高年级本科生。压缩包共10个文件约779KB以 tinyscan.cpp 源程序为核心配合 t1~t3.tny 三类输入用例、dev 与 layout 工程配置文件和 Windows 平台可执行文件另有实验报告 docx 记录 DFA 设计、状态转移与结果验证。已有 1333 人学习下载。读者可从中获得一套可编译运行的词法分析流程从源码读入、标识符与整数常量的识别到关键字、运算符的 token 划分与结构化输出并可对照报告理解确定有限自动机的状态设计与常见错误排查思路便于迁移到自己的实验环境中改写和扩展。1. 从一段 TINY 源码说起词法分析器到底替你挡了什么TINY 语言的词法分析器是编译原理课设里最容易被低估的一环看起来只是把字符切成单词真写起来却会卡在关键字查表、最长匹配、注释跨行和行号维护上。这个压缩包走的是 C/C 手工构造路线不依赖 lex/flex 生成器适合想看清扫描器内部状态流转的人也适合需要一份能改、能调试、能交作业的底稿的读者。下面按 token 定义、扫描循环、构建测试、避坑排查、验证接入五步拆开重点放在参数边界和翻车点而不是泛泛讲编译流程。2. 拆开压缩包前先定 tokenTINY 词法单元的边界与 C/C 数据结构很多人拿到资源先打开 main.c 从第一行读结果被状态机绕晕。词法分析器的源码不是按文件顺序读的而是按 token 表读的。先把 TINY 的词法边界钉死再回头看 C/C 代码哪些分支该存在、哪些状态该合并就一目了然。这一章先把 token 分类、数据结构和你为什么要手工写讲清楚后面读扫描循环才不会靠猜。2.1 TINY 的字符集与 token 分类关键字、标识符、数字、符号、注释TINY 语言是编译原理教材里常见的教学子集词法层通常只认几类东西关键字、普通标识符、无符号十进制整数、单字符或双字符运算符、分隔符、花括号注释以及文件结束符。它的关键字表一般包括if、then、else、end、repeat、until、read、write、do运算符包括、-、*、/、、、、:分隔符包括(、)、;。注释用{ ... }包起来不支持嵌套。这些边界如果不在纸上写清楚写扫描器时就会把:拆成:和或者把ifx误认成关键字if加标识符x。词法单元常见形式要点关键字if、then、else、end、repeat、until、read、write、do小写敏感先收满标识符再查表标识符字母或下划线开头后接字母、数字、下划线长度要有上限不能无限拷贝整数连续十进制数字负数交给语法层词法层不吞-赋值号:双字符必须最长匹配比较符、、单字符别和赋值号混注释{ ... }不嵌套必须处理未闭合空白空格、制表、回车、换行跳过但维护行号列号这张表里最容易被忽略的是“负数交给语法层”。很多新手看到-3就想在词法分析器里直接返回一个负数 token结果后面语法分析处理一元负号时又要拆开。常见做法是词法层只返回TOK_MINUS和TOK_NUM由语法分析决定它是不是负数字面量。TINY 的整数范围也要提前约定课程设计里常见做法是用int承接但如果测试用例里出现超过INT_MAX的数字atoi会悄悄溢出这类问题不会在正常样例里暴露属于典型的后悔药场景。行号和列号从 1 开始还是从 0 开始也要在 token 结构里定死。我一般让行号列号都从 1 开始因为报错信息里写“第 3 行第 7 列”比“第 2 行第 6 列”更符合人的阅读习惯。EOF 的列号取当前游标位置即可不要写成 0否则后面做错误定位时会出现负数列号。注释里的换行也要让行号递增不能因为注释被跳过就把行号吞掉否则报错位置会整体偏移。2.2 用 C/C 结构体承接扫描结果TokenType、Token、行号列号TINY 的词法分析器在 C/C 里通常用enum表示 token 类型用结构体把类型、词素、位置和附加值打包。这里有一个选型细节lexeme是用const char *指向输入缓冲区还是用固定数组拷贝一份。前者省内存但输入缓冲区一旦释放token 里的指针就悬空后者多拷贝几十字节但调试和后续语法分析省心。课程设计场景里我一般选固定数组长度设成 64 或 128超长标识符直接报错或截断别让一个测试用例把栈写穿。/* token.hTINY 词法单元定义固定数组避免悬空指针 */ #ifndef TOKEN_H #define TOKEN_H #define MAX_LEXEME 64 typedef enum { TOK_EOF 0, /* 0 方便循环判断 */ TOK_ID, TOK_NUM, TOK_IF, TOK_THEN, TOK_ELSE, TOK_END, TOK_REPEAT, TOK_UNTIL, TOK_READ, TOK_WRITE, TOK_DO, TOK_ASSIGN, /* : */ TOK_EQ, /* */ TOK_LT, /* */ TOK_GT, /* */ TOK_PLUS, TOK_MINUS, TOK_STAR, TOK_SLASH, TOK_LPAREN, TOK_RPAREN, TOK_SEMI, TOK_ERROR } TokenType; typedef struct { TokenType type; char lexeme[MAX_LEXEME]; /* 词素副本生命周期跟随 Token */ int line; /* 起始行号从 1 开始 */ int col; /* 起始列号从 1 开始 */ int ival; /* 仅 TOK_NUM 有效 */ } Token; const char *token_name(TokenType type); #endif这段定义里TOK_EOF 0不是随手写的。后面写while (tok.type ! TOK_EOF)或者用if (!tok.type)判断结束时0 值语义清晰。lexeme用char[MAX_LEXEME]之后make_token里用snprintf拷贝长度超过MAX_LEXEME - 1就截断并返回TOK_ERROR不要静默截断后继续当正常标识符用。line和col记录的是 token 第一个字符的位置不是扫描结束时的位置这一点在报错时非常关键。ival只在数字 token 里有效其他类型置 0避免调试输出里出现随机值。token_name是一个小函数把枚举转成可读字符串测试脚本比对输出时全靠它。常见做法是用一个switch返回ID、NUM、ASSIGN这类名字不要直接打印枚举数字。数字 token 的ival用strtol承接比atoi多一个错误检查errno被设置或者endptr没走到词素末尾就返回TOK_ERROR这样超范围数字不会悄悄变成负数。2.3 为什么手工写扫描器而不是直接上 lex/flex课程设计与可控调试的取舍这个资源的关键词里有“手工构造”这不是为了避开工具而避开工具而是词法分析器这个环节特别适合看清状态推进。lex/flex 用正则规则生成扫描器优点是规则改动快缺点是生成出来的 C 代码层层跳转调试时很难把某个 token 的识别过程和源码对应上。课程设计里经常要求你说明“标识符状态怎么走到关键字查表”“注释状态怎么处理 EOF”手工写一遍比看生成代码更有说服力。维度手工扫描器lex/flex 生成代码可读性高状态和分支都在手边低生成代码跳转多行号列号维护自己控制灵活需要额外写YY_USER_ACTION最长匹配自己写容易漏规则引擎保证课程设计适配适合讲清原理容易被问“生成代码怎么工作”扩展关键字改枚举和关键字表改规则后重新生成手工写最大的风险是最长匹配和回退。比如:必须先把:吃掉再看下一个字符是不是如果是吃掉返回TOK_ASSIGN如果不是就要返回TOK_ERROR因为 TINY 里单独的:不是合法 token。这里没有“回退一格”的问题因为第二个字符已经用peek看过了只有确认不是才不推进。真正需要回退的场景是数字后面跟字母比如123abc常见处理是数字扫描到a前停下返回TOK_NUM下一轮再报标识符错误而不是把整个123abc当一个 token。手工扫描器还要提前想好错误恢复。遇到非法字符时返回TOK_ERROR并记录位置主循环可以选择跳过该字符继续扫描也可以直接退出。课程设计里我一般让TOK_ERROR带上原始字符和行列号主循环打印后跳过当前字符继续这样一次运行能看到多个错误而不是遇到第一个就停。这个选择不影响词法正确性但影响调试体验。3. 手写扫描循环从字符流到 token 流的六个状态扫描器的骨架其实很小一个输入指针、一个行号列号、一个next_token函数。真正麻烦的是边界条件。下面这组代码把peek、advance、空白跳过、标识符、数字、注释和运算符分开写读的时候先看next_token的主循环再回头看每个分支。参数上我默认输入是以\0结尾的字符串文件读取时多留一个字节别让peek越界。3.1 输入缓冲与游标推进peek/advance 两个原语peek只读当前字符不推进advance返回当前字符并推进遇到\n把行号加一、列号归 1其他字符列号加一。EOF 时advance不推进否则pos会一直涨后面算词素长度时就越界。这个细节看起来小但注释未闭合时如果advance在\0上继续推进程序会跑进未知内存表现为随机崩溃或者死循环属于血泪经验里最常见的一类。/* scanner.c游标原语输入必须以 \0 结尾 */ #include scanner.h #include ctype.h #include stdio.h #include stdlib.h #include string.h static const char *src; static size_t pos; static int line_no; static int col_no; void scanner_init(const char *input) { src input; pos 0; line_no 1; col_no 1; } static char peek(void) { return src[pos]; } static char advance(void) { char c src[pos]; if (c \0) { return c; /* EOF 不推进避免越界 */ } pos; if (c \n) { line_no; col_no 1; } else { col_no; } return c; }scanner_init只接收一个以\0结尾的字符串文件读取函数负责分配和释放。peek和advance都是static外部只能通过next_token拿 token。line_no和col_no是全局状态这样做的好处是代码短坏处是不能同时跑两个扫描器课程设计里够用如果后面要接多文件把这三个变量塞进Scanner结构体即可。advance返回的是推进前的字符这个语义要统一别在某个分支里返回推进后的字符否则词素截取会错位。3.2 标识符与关键字识别先收满再查表标识符识别的关键顺序是“先收满再查表”。如果一边读一边查关键字ifx会在读到if时就返回关键字剩下的x变成下一个标识符这是典型的翻车点。正确做法是先把字母、数字、下划线全部吃进去拿到完整词素再和关键字表逐项比较。关键字表用const数组顺序无所谓但大小写要一致TINY 的关键字通常全小写。typedef struct { const char *word; TokenType type; } Keyword; static const Keyword keywords[] { {if, TOK_IF}, {then, TOK_THEN}, {else, TOK_ELSE}, {end, TOK_END}, {repeat, TOK_REPEAT}, {until, TOK_UNTIL}, {read, TOK_READ}, {write, TOK_WRITE}, {do, TOK_DO} }; static Token make_token(TokenType type, size_t start, int line, int col) { Token t; size_t len pos - start; t.type type; t.line line; t.col col; t.ival 0; if (len MAX_LEXEME) { len MAX_LEXEME - 1; t.type TOK_ERROR; /* 超长词素直接报错不静默截断 */ } memcpy(t.lexeme, src start, len); t.lexeme[len] \0; return t; } static Token scan_identifier(size_t start, int line, int col) { while (isalnum((unsigned char)peek()) || peek() _) { advance(); } Token t make_token(TOK_ID, start, line, col); for (size_t i 0; i sizeof(keywords) / sizeof(keywords[0]); i) { if (strcmp(t.lexeme, keywords[i].word) 0) { t.type keywords[i].type; break; } } return t; }make_token的start是 token 第一个字符在src里的下标pos是扫描结束后的下标两者相减得到词素长度。len MAX_LEXEME时返回TOK_ERROR这样测试用例里的超长标识符不会把后面的 token 流带偏。关键字查表放在收满之后ifx会先变成TOK_ID查表不命中保持标识符if会命中TOK_IF。isalnum的参数强转unsigned char是防御性写法避免负值字符传入未定义行为。3.3 数字与运算符识别最长匹配和回退一格数字扫描比标识符简单只吃连续数字遇到非数字就停。ival用strtol转换转换前把errno置 0转换后检查errno ERANGE或者endptr是否指向词素末尾。运算符里唯一需要最长匹配的是:其他都是单字符。如果你在实验里扩展、照:的写法先advance看下一个字符再决定返回双字符还是单字符别先返回再让变成下一个 token。static Token scan_number(size_t start, int line, int col) { while (isdigit((unsigned char)peek())) { advance(); } Token t make_token(TOK_NUM, start, line, col); errno 0; char *end NULL; long v strtol(t.lexeme, end, 10); if (errno ERANGE || end t.lexeme || *end ! \0) { t.type TOK_ERROR; t.ival 0; } else { t.ival (int)v; } return t; } static Token scan_operator(size_t start, int line, int col) { char c advance(); switch (c) { case :: if (peek() ) { advance(); return make_token(TOK_ASSIGN, start, line, col); } return make_token(TOK_ERROR, start, line, col); case : return make_token(TOK_EQ, start, line, col); case : return make_token(TOK_LT, start, line, col); case : return make_token(TOK_GT, start, line, col); case : return make_token(TOK_PLUS, start, line, col); case -: return make_token(TOK_MINUS, start, line, col); case *: return make_token(TOK_STAR, start, line, col); case /: return make_token(TOK_SLASH, start, line, col); case (: return make_token(TOK_LPAREN, start, line, col); case ): return make_token(TOK_RPAREN, start, line, col); case ;: return make_token(TOK_SEMI, start, line, col); default: return make_token(TOK_ERROR, start, line, col); } }scan_number里strtol的end必须指向\0否则说明词素里混进了非数字字符直接报错。ival用int承接如果实验要求支持更大整数把Token.ival改成long同时把打印格式从%d改成%ld。scan_operator的start在调用前已经记录advance先吃掉第一个字符再看peek决定要不要吃第二个。:后面不是时返回TOK_ERROR因为 TINY 没有单独的冒号 token这个错误位置就是冒号本身方便报错。3.4 注释、空白与行号维护别让 EOF 变成玄学注释处理要同时检查}和\0不能只写while (peek() ! }) advance();否则未闭合注释会把游标推到字符串末尾之外。跳过注释后不要递归调用next_token长注释嵌套多时递归会加深调用栈用外层for (;;)循环继续扫下一个 token 更稳。空白跳过时\n已经由advance维护行号不需要额外加。Token next_token(void) { for (;;) { while (peek() || peek() \t || peek() \r || peek() \n) { advance(); } int start_line line_no; int start_col col_no; size_t start pos; char c peek(); if (c \0) { return make_token(TOK_EOF, start, start_line, start_col); } if (isalpha((unsigned char)c) || c _) { return scan_identifier(start, start_line, start_col); } if (isdigit((unsigned char)c)) { return scan_number(start, start_line, start_col); } if (c {) { advance(); while (peek() ! \0 peek() ! }) { advance(); } if (peek() }) { advance(); continue; /* 注释不产生 token */ } return make_token(TOK_ERROR, start, start_line, start_col); } return scan_operator(start, start_line, start_col); } }next_token的start_line和start_col放在空白跳过之后、token 扫描之前这样read x;里x的列号是实际列号不是行首列号。c {分支里先advance吃掉左花括号再循环到右花括号或 EOF遇到 EOF 返回TOK_ERROR错误位置是注释起始位置报错信息里能直接指出“第几行第几列注释未闭合”。continue回到循环顶部继续跳空白和扫下一个 token不会产生注释 token。EOF 的make_token里pos - start为 0lexeme是空串主循环判断TOK_EOF后退出即可。4. 把扫描器跑起来构建、测试用例与 token 输出对照代码写完不等于能交。词法分析器最怕“看起来能跑”实际 token 流里多一个少一个后面语法分析全乱。下面给一个最小 Makefile、一个 token dump 主程序、一个 TINY 样例和批量回归脚本。构建参数先开-Wall -Wextra再开-fsanitizeaddress,undefined跑一轮很多越界和未初始化问题会在测试阶段直接暴露。4.1 用 CMake 或 Makefile 组织最小工程三个文件起步课程设计里常见做法是把词法分析器拆成token.h、scanner.h、scanner.c、main.c编译单元不用多。Makefile 里把头文件依赖写进模式规则改token.h后所有相关.o都会重编避免“改了枚举但旧目标文件没更新”的玄学问题。CFLAGS里的-stdc11保证strtol、snprintf这些函数声明可见-g方便 gdb 看游标。CC gcc CFLAGS -stdc11 -Wall -Wextra -g TARGET tiny_lexer OBJS main.o scanner.o $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $ $^ %.o: %.c token.h scanner.h $(CC) $(CFLAGS) -c $ -o $ clean: rm -f $(OBJS) $(TARGET) asan: $(CC) -stdc11 -Wall -Wextra -g -fsanitizeaddress,undefined \ main.c scanner.c -o $(TARGET)_asanOBJS只列参与链接的目标文件%.o规则把token.h和scanner.h作为依赖头文件改动会触发重编。asan目标不覆盖普通构建生成tiny_lexer_asan跑完测试后直接删掉即可。如果你的环境没有gcc换成clang也一样-fsanitize参数通用。Windows 下用 MinGW 或 WSL 都行注意路径分隔符别写死。4.2 准备 TINY 样例与期望输出逐行比对 token 序列样例要覆盖关键字、标识符、数字、赋值号、注释、空白和 EOF。下面这个sample.tiny里故意放了sum : 0;和x 0前者是赋值后者是比较词法层必须区分TOK_ASSIGN和TOK_EQ。注释放在第一行用来检查注释跳过后行号有没有偏。{ sample.tiny } read x; sum : 0; repeat sum : sum x; x : x - 1; until x 0; write sum;期望输出不用手写全部先跑一遍 token dump人工确认前几行和关键行再把输出固化成.expected文件。主程序里token_name打印类型lexeme打印词素line:col打印位置数字额外打印ival。TOK_ERROR和TOK_EOF也打印EOF 作为结束标记保留在输出里回归比对时能看出有没有提前退出。/* main.c读取文件并逐 token 打印便于和期望输出 diff */ #include scanner.h #include stdio.h #include stdlib.h static char *read_file(const char *path) { FILE *fp fopen(path, rb); if (!fp) return NULL; fseek(fp, 0, SEEK_END); long n ftell(fp); fseek(fp, 0, SEEK_SET); char *buf malloc((size_t)n 1); if (!buf) { fclose(fp); return NULL; } size_t got fread(buf, 1, (size_t)n, fp); buf[got] \0; fclose(fp); return buf; } int main(int argc, char **argv) { if (argc ! 2) { fprintf(stderr, usage: %s file.tiny\n, argv[0]); return 2; } char *buf read_file(argv[1]); if (!buf) { fprintf(stderr, cannot read %s\n, argv[1]); return 1; } scanner_init(buf); for (;;) { Token t next_token(); printf(%-10s %-16s %d:%d, token_name(t.type), t.lexeme, t.line, t.col); if (t.type TOK_NUM) { printf( ival%d, t.ival); } printf(\n); if (t.type TOK_EOF || t.type TOK_ERROR) { break; } } free(buf); return 0; }read_file用rb打开避免 Windows 下文本模式把\r\n转成\n导致列号对不上。fseek和ftell拿到文件长度后多分配一个字节放\0fread的返回值got才是实际读到的字节数不要直接用n当长度。主循环遇到TOK_EOF或TOK_ERROR就退出如果你希望错误后继续扫把TOK_ERROR的break去掉即可但回归脚本要相应调整期望输出。4.3 用脚本做回归批量跑样例并 diff 结果手工比对三五个样例还行样例一多就会漏。用 Bash 写一个回归脚本遍历tests/*.tiny把实际输出写到.actual再和.expected做diff -u。脚本开头set -euo pipefail任何一个样例失败就停diff的非零退出码会直接中断避免“后面全错但脚本还显示通过”。#!/usr/bin/env bash set -euo pipefail for f in tests/*.tiny; do base${f%.tiny} ./tiny_lexer $f $base.actual diff -u $base.expected $base.actual done echo all lexer cases passedbase${f%.tiny}去掉扩展名得到tests/sample期望文件就是tests/sample.expected。diff -u输出统一格式失败时能看到哪一行 token 多了或少了。脚本最后一行打印通过信息不要写成夸张的“完美通过”只是告诉你所有diff返回 0。新增样例时先跑一次生成.actual人工确认后复制成.expected不要直接拿程序输出当标准答案否则错了也会被固化。5. 避坑与排查手工词法分析器最容易翻车的五处词法分析器的 bug 有个特点编译能过运行不崩token 流却悄悄错位。下面五类问题在课程设计和自测里出现频率最高每条按“现象、原因、解决”写。调试时别一上来就加printf满屏输出先把最小复现样例缩到十行以内再打开 token dump 对照位置。5.1 五类高频翻车现场现象、原因、解决第一类关键字被拆错。现象是ifx输出成IF加ID(x)或者repeat1被识别成REPEAT加NUM(1)。原因是扫描标识符时边读边查关键字读到if就提前返回没有等标识符收满。解决是把关键字查表放到scan_identifier收满全部字母数字下划线之后先用TOK_ID构造 token再用strcmp逐项比较关键字表命中才改类型。改完后用ifx、if、iff、repeat1四个输入各跑一遍确认只有完整的if和repeat变成关键字。第二类注释未闭合导致越界或死循环。现象是输入以{开头但没有}时程序不报错直接卡住或者打印一堆乱码。原因是注释循环只写了while (peek() ! }) advance();没有检查\0advance在 EOF 处如果继续推进就会跑出缓冲区。解决是把循环条件改成while (peek() ! \0 peek() ! })循环结束后判断peek()是不是}不是就返回TOK_ERROR错误位置记在注释起始处。测试用例里专门放一个bad_comment.tiny内容只有{ unclosed确认程序能正常退出并报错。第三类:被拆成两个 token。现象是sum : 0;输出ERROR(:)加EQ加NUM或者COLON加EQ。原因是运算符分支先返回了单字符:没有看下一个字符是不是。解决是在case :里先advance()吃掉冒号再peek()看下一个字符是就再advance()并返回TOK_ASSIGN不是就返回TOK_ERROR。如果你扩展了、同样处理先看第二个字符再决定返回双字符还是单字符。注意不要先返回再指望下一轮把吃掉那样 token 流已经错了。第四类行号列号整体偏移。现象是报错信息里的行号比实际少 1或者列号总是从 0 开始。原因是start_line和start_col记录位置太早在跳过空白之前就记了导致 token 位置是行首而不是 token 首字符或者advance在\n时先加列号再加行号顺序反了。解决是把start_line、start_col放在空白跳过之后、peek取第一个有效字符之前advance里遇到\n先把行号加一、列号归 1其他字符列号加一。用read x;验证read应该是 2:1x应该是 2:6;应该是 2:7。如果对不上先打印pos和line_no对照源码数一遍。第五类词素指针悬空或超长截断。现象是调试时 token 的lexeme前一秒还对函数返回后变成乱码或者超长标识符把后面的 token 挤掉。原因是lexeme用了const char *指向局部缓冲区函数返回后缓冲区失效或者固定数组拷贝时没有检查长度memcpy写越界。解决是Token.lexeme用固定数组make_token里先算len pos - startlen MAX_LEXEME就截断并返回TOK_ERROR正常长度用memcpy加\0。测试时放一个 100 字符的标识符确认输出ERROR而不是崩溃再放一个 63 字符的标识符确认能完整保留。5.2 调试手段token dump、字符码打印、最小复现遇到 token 错位第一步不是改代码是缩小输入。把出问题的 TINY 源码删到只剩触发错误的那一行比如只留sum : 0;跑 token dump 看输出。如果这一行没问题再逐行加回去直到错误复现。这个办法看着笨但比盯着几百行源码猜要快。第二步是打印字符码尤其是不可见字符和中文标点。TINY 源码里混进全角分号或全角括号时scan_operator会走default返回TOK_ERROR但肉眼看源码很难发现。用od -c sample.tiny或者在小工具里打印(int)c能直接看到字节值。第三步是开 sanitizer。make asan生成带 AddressSanitizer 和 UBSan 的二进制跑一遍全部样例。注释未闭合、数组越界、strtol溢出这类问题会在 sanitizer 下直接报栈比事后猜“为什么偶尔崩”省时间。第四步是给next_token加一个调试开关打印每次进入时的pos、line_no、col_no和peek()但不要长期留在提交代码里输出太多反而看不清。调试完把开关关掉回归脚本重新跑一遍。6. 进阶与验证把词法器接进语法分析前的自检清单词法分析器写完最后一个 token 函数别急着往语法分析器里塞。先做一轮自检把 token 流当成接口来验。下面这张表是我每次交课程设计前会过一遍的清单按“自检项、做法、通过标准”组织适合直接抄到实验报告里也适合当回归用例的补充。自检项做法通过标准关键字边界输入if、ifx、iff、repeat1只有完整关键字变类型其余是ID最长匹配输入:、:、、若扩展:不被拆开单字符不吞下一个注释闭合输入{ ok }和{ unclosed前者跳过后者ERROR且不越界行号列号输入多行read x;每个 token 位置和源码对得上数字溢出输入超过INT_MAX的数字返回ERROR不静默变负数超长词素输入 100 字符标识符返回ERROR不写越界EOF 处理空文件和只有空白文件只输出EOF位置合理错误恢复输入和sum : 0;报错后能继续扫或明确退出这张表里“错误恢复”最容易被忽略。课程设计里常见要求是“遇到错误继续”但继续的方式要一致TOK_ERROR打印后跳过当前字符还是直接退出两种都行但回归脚本的期望输出要对应。我一般选“打印后跳过当前字符继续”这样一次运行能看到多个错误调试效率高。跳过时注意advance只跳一个字符如果错误字符是多字节的别用pos 2这种硬编码让advance按字节走。进阶用法可以加一个 token 覆盖率统计跑完样例后统计TOK_IF、TOK_ASSIGN、TOK_ERROR各出现多少次确认测试用例真的覆盖了关键字和双字符运算符。这个统计不用改扫描器在main.c里加一个计数数组按t.type累加最后打印非零项。它不能证明词法分析器绝对正确但能暴露“样例里根本没测到:”这种盲区。从那以后我每次交词法分析器前都先跑最小样例、注释未闭合样例和最长匹配样例再打开 sanitizer 跑一轮最后对照 token 位置表逐行数一遍。词法分析器不怕写得慢怕的是 token 流悄悄错位后面语法分析全成黑匣子。希望帮到你。本文还有配套的精品资源点击获取

相关推荐

链表核心操作与LeetCode热题解析
链表核心操作与LeetCode热题解析

1. 为什么 LeetCode 热题 100 中的链表如此重要? 链表作为数据结构中的基础类型,在技术面试中的出现频率高达 78%(根据 2023 年一线互联网企业面试题库统计)。不同于数组的连续存储特性,链表通过节点间的指针连接实现动… · 2026/11/2 11:57:03

EF+WinForms 可修改数据库连接与附带 SQLite 库交付
EF+WinForms 可修改数据库连接与附带 SQLite 库交付

简介:这份压缩包面向使用 .NET 开发 Windows 桌面应用的初中级开发者,围绕 Entity Framework 与 WinForms 的结合,演示如何在程序中读取、修改并保存数据库连接配置,同时附带可直接配合使用的数据库文件,适合需要理解配… · 2026/11/2 11:57:03

HuggingFace模型下载与离线部署:超时断流、缓存校验实战
HuggingFace模型下载与离线部署:超时断流、缓存校验实战

模型下不动、卡在 99%、ConnectionError反复弹出来,这几个场景只要用过 HuggingFace,基本都躲不开。我把这两年折腾模型分发攒下来的经验整理成这篇,覆盖超时连接、本地下载、离线加载、缓存目录结构和一堆奇奇怪怪的报错。不管你是刚装完tra… · 2026/11/2 11:57:03

HappyPlanet元宇宙平台:低门槛3D社交与创作体验
HappyPlanet元宇宙平台:低门槛3D社交与创作体验

1. HappyPlanet平台概述:元宇宙的平民化入口HappyPlanet作为新兴的元宇宙平台,其核心定位是降低普通用户进入元宇宙的门槛。与需要高端VR设备或复杂操作的传统平台不同,HappyPlanet通过网页端和轻量级App即可访问,这种"低配置… · 2026/11/3 5:57:12

Univer开源在线表格集成实践:Canvas渲染、协同编辑与二次开发解析
Univer开源在线表格集成实践:Canvas渲染、协同编辑与二次开发解析

Univer这个项目在开发者圈子里突然热起来,最近不少人在讨论Univer在线表格怎么集成、能不能替代自研表格引擎。我自己从第一版开始就跟过这个开源项目,从早期只有Sheet能力,到后来扩展出Doc和Slide,一路踩了不少坑,也积… · 2026/11/3 5:57:12

补码的本质是模运算:二进制有符号数的数学根基与硬件实现
补码的本质是模运算:二进制有符号数的数学根基与硬件实现

简介:本资源是《计算机组成原理(第三版)》配套习题的完整参考答案,面向高校计算机类专业本科生及考研复习者,重点解决课后习题理解难、标准解法不明确、数值表示与系统结构概念易混淆等学习痛点。答案覆盖全书核心章节… · 2026/11/3 5:57:12

欧美汽车消费差异:小排量与大排量的文化技术对比
欧美汽车消费差异:小排量与大排量的文化技术对比

1. 欧洲与美国汽车消费差异现象解析走在欧洲街头,你会发现一个有趣的现象:道路上行驶的车辆普遍比美国小一圈,发动机排量也明显更小。这不是偶然的巧合,而是根植于两地不同汽车文化的必然结果。作为在汽车行业深耕多年的从业者&am… · 2026/11/3 5:57:12

鸿蒙UI开发:@Builder与@LocalBuilder构建器模式详解
鸿蒙UI开发:@Builder与@LocalBuilder构建器模式详解

1. 鸿蒙UI开发中的构建器模式解析在鸿蒙应用开发中,Builder和LocalBuilder是两种常用的轻量级UI元素构建方式。它们本质上都是ArkTS语言提供的装饰器,用于封装UI元素的构建逻辑,但在作用域和性能优化方面存在关键差异。1.1 构建器的基本概念构… · 2026/11/3 5:57:12

ABAP内表导出Excel的5种实战方案与选型指南
ABAP内表导出Excel的5种实战方案与选型指南

1. 为什么ABAP内表导出Excel这件事,十年来始终是开发现场的“高频堵点”在某公司SAP系统维护团队里,我见过太多次这样的场景:业务部门凌晨发来加急需求——“请把上月销售明细导出成Excel,领导要马上看”,开发同事立刻… · 2026/11/3 5:56:11

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46

STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59

ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59

基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码