分词器需识别标识符、数字、运算符、括号、字符串字面量等边界,采用状态驱动机制;使用 std::string_view 避免重复拷贝,确保源码生命周期长于 token。

分词器要处理哪些典型字符边界
分词器不是简单按空格切,得识别标识符、数字、运算符、括号、字符串字面量等边界。比如 "a+b*2.5" 应拆成 "a"、"+"、"b"、"*"、"2.5";而 "x = "hello\n";" 中的 ""hello\n"" 必须整体当一个 STRING token,不能被反斜杠或换行打断。
关键在于状态驱动:遇到引号进字符串态,遇到数字开头进数字态,否则默认归为标识符或单字符运算符。一旦状态切换,就提交前一个 token。
- 字母/下划线开头 → 标识符(直到非字母数字/下划线为止)
- 数字开头 → 数字字面量(支持小数点和科学计数法前缀,如
"123"、"3.14"、"1e-2") -
"'"或"""开头 → 字符串/字符字面量(需处理转义,如"\n"、"\"") - 常见单字符运算符(
"+"、"-"、"*"、"/"、"="、"("、")"等)直接作为 token - 跳过空白(空格、制表、换行),但换行需记录行号用于后续错误定位
如何用 std::string_view 避免重复拷贝
每次切出 token 都 new 一个 std::string?没必要。原始代码通常是一次性加载的完整源码,用 std::string_view 只存起始指针和长度,既快又省内存。token 结构体里存 std::string_view,而不是复制内容。
注意:必须确保原始字符串生命周期长于所有 token 的使用周期——比如把源码存在 Tokenizer 类成员里,再让 token 引用它。
立即学习“C++免费学习笔记(深入)”;
- 构造
std::string_view时用source.substr(start, len),别用std::string(source.begin()+start, source.begin()+end) - 如果后续要做哈希或比较,
std::string_view::compare()比==更安全(避免隐式转换) - 调试时打印 token 内容可用
sv.data()+sv.size(),不要直接 cout
怎么处理双字符运算符(如 "=="、"!="、")
顺序很重要。必须先检查双字符组合,再回落到单字符。比如读到 '=',不能立刻生成 ASSIGN token,得 peek 下一个字符:如果是另一个 '=',就合并成 EQ;否则才是 ASSIGN。
常见陷阱是没处理好 peek 失败(如到了末尾)或忘记回退。推荐用索引而非迭代器,方便控制位置:
if (i + 1 < source.length() && source[i] == '=' && source[i+1] == '=') {
tokens.emplace_back(EQ, std::string_view(&source[i], 2));
i += 2;
}
- 所有双字符判断都放在单字符之前(
"=="在"="前," 在 <code>" 前) - peek 后若不匹配,**不要跳过当前字符**——要让它参与下一轮单字符判断
-
"//"和"/*"注释也属于双字符模式,但它们后面的内容应直接跳过,不生成任何 token
为什么行号和列号不能靠 std::count 实时计算
每遇到换行就 ++line,每步移动 ++col,遇到换行重置 col=0——这是唯一高效且准确的方式。用 std::count(source.begin(), source.begin()+i, '
') 看似简洁,但每次调用都是 O(n) 扫描,整个 tokenize 变成 O(n²),大文件直接卡死。
- 初始化
line = 1,col = 1(按人类习惯,首行首列为 1) - 每次
++i后检查source[i-1] == ' ':是则line++、col = 1;否则col++ - token 结构体里存
line和col,不是偏移量。这样报错时能准确定位error at line 5, col 12
真正麻烦的是 UTF-8:一个中文字符占 3 字节,但列号应按 Unicode 字符数算,不是字节数。简单分词器可忽略这点,但若要支持中文标识符,就得用 std::codecvt_utf8 或第三方库解码后再计列——多数 C++ 教学分词器直接限定 ASCII 输入,反而更稳。


















