应使用 std::string_view 做输入切片以避免重复拷贝,Token 中仅存 string_view 和类型枚举;关键字用静态 unordered_map 查表;数字字面量须手工状态机解析;注释需完整吞掉并更新行号;非法字符宜跳过并记录警告。

用 std::string_view 做输入切片,避免重复拷贝
词法分析器本质是「向前扫描 + 模式匹配」,源码字符串本身不该被修改或反复 substr。用 std::string_view 指向原始文本,配合下标移动(pos)和长度截取,既零拷贝又清晰。别用 std::string 存每个 token 的内容——token 里只需存 std::string_view 和类型枚举,真正内容永远指向原输入缓冲区。
常见错误:在循环里写 auto token_str = input.substr(pos, len),导致每次构造新字符串;尤其处理大文件时内存暴涨、缓存不友好。
- 初始化时传入
std::string_view source,全程只操作这个视图 - 每个
Token结构体里用std::string_view lexeme字段,不复制数据 - 跳过空白/注释时,直接递增
pos,不生成 token
识别关键字必须用查找表,别写一长串 if-else if
关键字(如 if、while、return)和标识符共享同一类文法(字母/下划线开头 + 字母数字下划线),但语义不同。如果先按标识符规则读完再判断是不是关键字,就必须查表;硬编码 if (s == "if") ... else if (s == "else") ... 在 C++20 前性能差,且易漏、难维护。
推荐用 std::unordered_map<:string_view tokentype></:string_view> 静态初始化关键字表,构造函数里用 constexpr 或 static const 初始化。注意:C++17 起 std::string_view 可作 unordered_map key,但需确认编译器支持(GCC 7+ / Clang 5+)。
立即学习“C++免费学习笔记(深入)”;
- 关键字表应只包含语言规范定义的关键字,不包括内置类型名(如
int)除非它们确实是保留字 - 查表前先确认当前
lexeme长度是否在关键字最大长度范围内,避免无效查找 - 查不到就归为
TOKEN_IDENTIFIER,不报错
数字字面量要区分整数、浮点、十六进制,且拒绝非法后缀
C++ 词法规范里,123、0xFF、3.14、1e2 是合法数字,但 0xG1、123.45.67、123f(无小数点或 e)是非法的——Lexer 不负责语义检查,但必须拒绝明显违反词法规则的输入。
典型陷阱:用 std::stod 或 std::stoi 解析数字字面量。这会掩盖格式错误(比如返回 0)、触发异常、且无法定位错误位置。Lexer 应纯手工状态机识别:0x 开头走十六进制分支,遇到非 0-9a-fA-F 立即报错;小数点后必须有数字,e/E 后必须有可选符号 + 数字。
- 整数部分为空的
.123是合法浮点(C++ 允许),不能跳过 -
0x后无数字(如0x单独出现)应报LEXER_ERROR_INVALID_HEX - 不接受
U、L等后缀——那是 Parser 或 Semantic Analyzer 的事,Lexer 只切出完整字面量
单行/多行注释必须吃掉全部内容,且不生成任何 token
注释不是 token,也不是空白;它是语法噪音,必须被 Lexer 完全吞掉,并推进读取位置。误把注释当空白处理(只跳过 // 后换行符)会导致后续行号错乱;更糟的是把 /* */ 中间内容当普通字符继续解析。
多行注释难点在嵌套检测缺失(C++ 不允许嵌套),但需处理 /* ... */ 跨行、中间含 * 或 / 的情况。正确做法是:匹配到 /* 后,逐字符扫描,直到遇到 */;期间遇换行要更新行号计数器;若到文件末尾都没找到 */,报 LEXER_ERROR_UNCLOSED_COMMENT。
-
//注释只需读到下一个\n或 EOF,不关心中间有没有\r - 所有注释逻辑必须在主 token 循环最前面执行,确保注释不会干扰关键字/标识符识别
- 别尝试用正则——C++ 标准库 regex 在短字符串上开销大,且难以控制错误位置
最难的不是识别某个 token,而是错误恢复:比如遇到 @ 这种非法字符,该停在原地报错,还是跳过它继续?实际工程中通常跳过单个非法字符并记录警告,否则一个错字会让整个文件后续全错。但行号、列号、源位置信息必须始终准确——这是调试体验的底线。


















