想象一位五星级餐厅后厨的管事。每天早晨,后厨会收到一整卷未切分的培根、整棵的葱和成块的姜。不管今天要做哪道菜,第一步永远是同一件事:把这些大块食材切成统一小料——葱花、姜末、培根丁。而在编译器世界里,这件事的名字叫“词法分析”:把一长串源代码字符,切成一个个有意义的词法单元(Token)。 Lex 与 Flex 就是两家“切菜流水线定制工厂”。你递给工厂一张规格书(.l 文件),工厂就为你造出一条全自动切料流水线(C 代码),从此源源不断地把原始字符流切成标准 Token 流。本文将从这家工厂的入门参观开始,带你认识规格书的版式、识别镜(正则)、动作按钮(动作函数),一直走到多机协同的先进车间(可重入扫描器)。

岗位关联

掌握 Lex/Flex,等于拿到了编译器前端最关键的“量产工具”——无论是开发语言工具链、日志解析器还是安全扫描器,词法分析都是不可或缺的地基,而这套技能几乎是所有编译工程师的起点。

目录
  1. 可视化演示
  2. 工具简介与安装
  3. 规则文件结构
  4. 正则表达式语法
  5. 动作函数编写
  6. 开始条件状态
  7. Flex高级特性
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

Lex与Flex是经典的词法分析器生成工具,通过规则文件定义正则与动作自动生成C词法分析器。本文讲解工具安装配... 切换模式查看不同维度。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Lex与Fl:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现

工具简介与安装

后厨要引进一条切菜流水线,但管事不打算自己焊机器。他只需要找一家靠谱的“流水线定制工厂”,把需求一说,工厂就交付整条产线。在词法分析领域,Lex 就是 1975 年诞生的那家老牌工厂,而 Flex 是它的开源增强版——同一套图纸语言的升级机型,切得更快、吃得更少、还免费。两者都接收同一种“规格书”(规则文件),工厂读懂后自动吐出你需要的 C 语言代码。

在编译原理的课程里,词法分析常常被当作“从字符流到 Token 流”的第一步。手动实现一个状态机倒也不难,但一旦你的语言有几十种 Token、上百条规则,手写代码就变得又臭又长、极易出错。Lex/Flex 的意义在于:你只需用正则描述“想切成什么样”,工具就把整个有限状态自动机翻译成 C 程序。几乎所有的现代编译器工具链中,Flex 都扮演着这个基础角色。

安装与上手出奇简单。Linux 下一条命令即可完成:sudo apt install flex;macOS 用户使用 brew install flex,Windows 可以选用 WinFlexBison。装完后敲 flex --version 验证版本。随后的典型流程只需三步:编写 fruit.l 文件,执行 flex fruit.l 生成 lex.yy.c,再用 gcc 链接编译得到可执行程序。整个流程像极了向工厂下单、工厂交货、后厨开箱三步曲。

看到这里,你已经完成了对“切菜流水线工厂”的第一次参观:认得工厂门牌、学会了订货流程。下一节我们翻开送进工厂的那张规格书,看看里面的固定版式。

fruit.l→  flex  lex.yy.c→  gcc  scanner\displaystyle \text{fruit.l}\xrightarrow{\;\texttt{flex}\;}\text{lex.yy.c}\xrightarrow{\;\texttt{gcc}\;}\text{scanner}
这串公式描述了词法工具的“下单-交付-组装”三步流水线:规格书经 flex 翻译为 C 源码,再由编译器变成可运行程序。
  1. 规格书输入fruit.l 是你写的规则文件,相当于给工厂的设计图纸,里面写着“哪些食材需要切、切成什么样”。
    fruit.l\displaystyle \text{fruit.l}
  2. flex 翻译动作运行 flex 命令,工厂读取图纸并按图纸施工,把正则规则翻译成 C 代码。
    flex\displaystyle \texttt{flex}
  3. 交付中间产物lex.yy.c 是工厂交付的流水线本体,一份完整的、可编译的 C 源文件。
    lex.yy.c\displaystyle \text{lex.yy.c}
  4. gcc 编译组装用 gcc 把 C 源码编译链接,得到最终可执行程序 scanner,流水线正式启动。
    gcc\displaystyle \texttt{gcc}

规则文件结构

流水线工厂里的每一张规格书,都有严格的版式要求,就像后厨墙面固定的三块白板:第一块写“今日备料清单”,第二块写“每样食材的加工步骤”,第三块写“遇到问题联系谁”。任何工人走错白板都会导致准备混乱。词法工具的规则文件 .l 也遵循类似的固定版式——它不是自由书写的作文纸,而是一份有明确分区的表格。

.l 文件由两个 %% 分隔成三段。第一段叫定义区,负责引入头文件、定义宏和声明开始条件;第二段叫规则区,每一行左侧写正则模式,右侧写匹配后的 C 动作代码;第三段叫用户代码区,存放主函数和辅助函数。Flex 就是按这个固定版式逐行读取并翻译成 C 程序。三段各有各的职责,混淆了顺序,工厂就会按错误图纸施工。

实际写起来非常直观。定义区最常见的开头是 %{ #include <stdio.h> %};规则区写 [0-9]+ { printf("数字!\n"); },意思是“遇到连续数字就打印”;用户代码区则放 int main(){ yylex(); return 0; }。需要注意两点:动作代码必须放在大括号 {} 里;若想忽略某些字符(比如空格),可以为它们写一条空动作规则,例如 " " ;,意思是“匹配到空格什么都不做”。

规格书的版式并不复杂,但三区的职责划分至关重要。记住这张“三块白板”的结构,你写出来的每一份 .l 文件都清清楚楚。接下来,我们走到规则区,仔细看看那副能认出任何食材的“识别镜”。

%{...%}⏟定义区  %%  正则  {动作}⏟规则区  %%  C函数⏟用户代码区\displaystyle \underbrace{\texttt{\%\{...\%\}}}_{\text{定义区}}\;\texttt{\%\%}\;\underbrace{\texttt{正则}\;\{\text{动作}\}}_{\text{规则区}}\;\texttt{\%\%}\;\underbrace{\texttt{C函数}}_{\text{用户代码区}}
这个公式把 .l 文件的三个区域和两个分隔符画在了一张图上:两头是定义区和用户代码区,中间夹着真正的核心——规则区。
  1. 定义区形如 %{ ... %} 的块,用来 #include 头文件、定义宏。相当于备料清单,提前把要用到的工具和食材备好。
    %{…%}\displaystyle \texttt{\%\{}\dots\texttt{\%\}}
  2. 第一次分隔第一个 %% 标志定义区结束、规则区开始,是规格书的第一个“分界石”。
    %%\displaystyle \texttt{\%\%}
  3. 规则区核心正则 { 动作 } 是规则区的最小单元,左边用正则描述“长什么样”,右边用 C 代码描述“抓到后干什么”。
    正则{动作}\displaystyle \texttt{正则}\{\text{动作}\}
  4. 第二次分隔第二个 %% 标志规则区结束、用户代码区开始,找到这里就找到了流水线的“售后页”。
    %%\displaystyle \texttt{\%\%}
  5. 用户代码区这里放 main() 函数和其他辅助函数,是流水线启动时的总控台。
    C函数\displaystyle \texttt{C函数}

正则表达式语法

什么样的机器能一眼认出“这是一段葱花”?答案是副厨手里那副特制识别镜。戴上它,所有食材在眼中只留下一个判断信号:是或不是目标类型。词法规则区左侧那串正则表达式,就是这副识别镜。它不关心具体某个字符“长什么样”,只关心“这一整段字符串的形状是否符合预期”——这正是一个词法模式最本质的抽象能力。

正则表达式的构建积木并不多,但组合起来威力巨大。. 匹配任意单个字符;* 表示前一项出现零次或多次;+ 表示前一项出现一次或多次;? 表示前一项可有可无;方括号 [abc] 是字符类,匹配其中任意一个字符,[^abc] 则相反;圆括号负责分组,竖线 | 表示“或”。把这些积木拼起来,你就能描述出千变万化的词法形状。

一个最经典的例子是 C 语言标识符(变量名):必须以字母或下划线开头,后面可以跟任意个字母、数字或下划线,于是模式写为 [a-zA-Z_][a-zA-Z0-9_]*。Flex 在匹配多个规则时还有一个重要策略:最长匹配优先——如果多个模式都能匹配,Flex 会选择“吃到文本最长”的那一个;若长度相同,才按规则文件中出现的先后顺序选择。这就像厨师面对“ab“和”ad”两片食材,会优先按最长的那块形状下刀。

戴上这副“标识符识别镜”,后厨一眼就能认出 count、_tmp2 是合法食材,而 2count 从第一眼就被拒之门外——因为开门条件就不满足。识别镜认出了食材,下一步自然是操作机械臂,把食材送进正确的料碗。这就是下一节的主角:动作函数。

[a−zA−Z_][a−zA−Z0−9_]∗\displaystyle [a-zA-Z\_][a-zA-Z0-9\_]^*
这就是 C 标识符的“识别镜”配方:第一个字符必须是字母或下划线,后续字符可以是字母、数字或下划线,且后面这部分出现任意多次都行。
  1. 第一组字符类[a-zA-Z\_] 匹配任意一个英文字母或下划线,这是标识符的“开门条件”,保证不能以数字开头。
    [a−zA−Z_]\displaystyle [a-zA-Z\_]
  2. 第二组字符类[a-zA-Z0-9\_] 匹配字母、数字或下划线,即标识符中允许出现的“身体零件”。
    [a−zA−Z0−9_]\displaystyle [a-zA-Z0-9\_]
  3. 星号量词* 表示前面这个字符类可以重复零次或多次,对应标识符后半段可有可无、可长可短。
    ∗\displaystyle *

动作函数编写

识别镜的任务只是“认出来”,真正干活的是认出来之后的那只手。在流水线上,每种食材对应一个“动作按钮”:切葱的按钮、切姜的按钮、装盘的按钮。词法规则区里,模式 { 动作 } 这对双胞胎正描绘了这个过程——左侧模式是识别镜,右侧动作就是那只按下按钮的手。

当 Flex 的扫描器匹配到一段文本,它会自动把这段文本放进一个叫 yytext 的字符指针变量里,同时把匹配长度写进 yyleng。你的动作代码可以随时读取这两个变量,完成分类、转换、返回等任务。动作代码是普通的 C 代码,可以写任意逻辑,但最常见的三种用途分别对应三种“按钮”:返回 Token 类型给上层语法分析器(return NUM;)、统计行号列号、忽略无意义的空白。

动作有一个关键行为需要理解:如果动作里没有 return,扫描器在动作执行完后会自动继续扫描下一个 Token。这就像流水线切完一片食材后不等待指令,自动送到下一片。正因如此,你需要在规则区末尾加上兜底规则 . ; 和 \n ;——前者忽略所有无法识别的单字符,后者忽略换行,否则非法字符会让扫描器陷入反复匹配失败的“死循环”。当输入耗尽时,yylex() 返回 0,整个流水线停机。

每一次匹配都是一次精准按键:葱花进葱碗、姜末进姜碗,后厨的秩序来源于这套“识别-动作”的对偶结构。但现实厨房还有一个挑战:某些食材在特定工序下才需要切割。比如遇到整段注释时,流水线应该切换到“忽略模式”。这就引出了词法工具的“工作模式”——开始条件状态。

Pi  ⟹  Ai(Pi=正则模式,Ai=C动作)\displaystyle P_i\;\Longrightarrow\;A_i\quad(P_i=\text{正则模式},A_i=\text{C动作})
这条对应关系是词法规则的精髓:当第 i 条规则的正则模式 P_i 匹配成功,就执行相应的动作 A_i——一看一按,识别与处理永远成对出现。
  1. 模式符号P_i 表示规则区第 i 条规则左侧的正则模式,即一种特定“食材形状”。
    Pi\displaystyle P_i
  2. 触发箭头双线箭头 \Longrightarrow 表示“若匹配成功,则强制触发”,这是规则成立的因果关系。
    ⟹\displaystyle \Longrightarrow
  3. 动作符号A_i 表示匹配成功后执行的 C 代码,比如返回 Token 类型或打印信息。
    Ai\displaystyle A_i

开始条件状态

高级后厨的流水线远远不止一种工作模式:平时逐颗切豆子,遇到整卷培根时切换到“大块模式”,切完再自动切回。如果一台机器永远只有一种模式,要么豆子被切稀碎,要么培根被切不动。词法分析同样会遇到“上下文相关”的难题——最典型的就是多行注释:/* 这是一段很长的注释 */ 中间的内容既不是关键字也不是数字,它们需要在“注释模式”下被整体忽略。

Flex 用开始条件(start condition)来解决这种问题。你可以用 %s COMMENT 声明一个名为 COMMENT 的新状态(%s 是包含式状态,%x 是排他式状态),默认状态名是 INITIAL。在规则的左侧加上 <COMMENT> 前缀,这条规则就只在 COMMENT 状态才生效;不加前缀的普通规则,则只在其他对它开放的状态下生效。负责切换模式的钥匙是 BEGIN(...) 函数。

举个例子:声明 %s COMMENT 后,规则 <COMMENT>.* { /* 忽略整行注释内容 */ } 会在注释状态下吞噬每一行内容;当扫描到 /* 时,动作里写 BEGIN(COMMENT) 就会让流水线进入注释模式;当扫描到 */ 时,动作里写 BEGIN(INITIAL) 切回默认模式。这个机制最有名的坑是:如果你进入了 COMMENT 状态却忘记切回,后续所有普通规则都会静默失效——整条流水线将“认不出”任何正常食材。

工作模式的切换让流水线变得聪明:该忽略的整段吞掉,该加工的一丝不漏。但到这里,我们的后厨还只有一条孤零零的流水线。现代厨房最壮观的一幕是几十条线同时开工、互不干扰。这就轮到 Flex 的高级武器登场了:可重入扫描器。

⟨INITIAL⟩→  BEGIN(COMMENT)  ⟨COMMENT⟩→  BEGIN(INITIAL)  ⟨INITIAL⟩\displaystyle \langle\text{INITIAL}\rangle\xrightarrow{\;\texttt{BEGIN(COMMENT)}\;}\langle\text{COMMENT}\rangle\xrightarrow{\;\texttt{BEGIN(INITIAL)}\;}\langle\text{INITIAL}\rangle
这条路径描绘了注释模式的完整旅程:从默认状态出发,由 BEGIN 切换进入注释状态,处理完毕后再由 BEGIN 复位回默认状态。
  1. 默认状态\langle INITIAL\rangle 是扫描器刚启动时的默认工作模式,所有普通规则在这里生效。
    ⟨INITIAL⟩\displaystyle \langle\text{INITIAL}\rangle
  2. 切换进注释执行 BEGIN(COMMENT) 后,流水线从默认模式切换到 COMMENT 模式,只有带 <COMMENT> 前缀的规则才被看见。
    BEGIN(COMMENT)\displaystyle \texttt{BEGIN(COMMENT)}
  3. 注释模式\langle COMMENT\rangle 状态中,注释内容按声明好的规则被吞噬或跳过。
    ⟨COMMENT⟩\displaystyle \langle\text{COMMENT}\rangle
  4. 切回复位执行 BEGIN(INITIAL) 把模式切回默认,否则普通规则永远不会再匹配——这是最常见的隐蔽故障。
    BEGIN(INITIAL)\displaystyle \texttt{BEGIN(INITIAL)}

Flex高级特性

现代后厨不会只接待一桌客人。二十桌同时点单,二十条流水线必须同时运转。每一条线都需要自己的料碗、自己的置物架、自己的识别镜参数——总不能二十条线抢同一副眼镜、同一个料碗。默认的 Flex 扫描器恰恰有这个问题:它的所有状态都存放在全局变量里(比如 yytext、yyleng),这就像所有机器共用一副眼镜,一台机器转了一下,其他全乱了套。

Flex 提供的解决方案叫可重入扫描器(reentrant scanner)。开启它只需要在规则文件顶部写一行 %option reentrant。开启后,扫描器的全部状态被封装进一个叫 yyscan_t 的结构体句柄中,通过参数在函数间传递,不再依赖全局变量。于是,你可以在一个程序里创建多个扫描器实例,每个实例维护独立的 Token 流——就像给每台机器装上独立控制芯片,彼此物理隔离。

具体用法非常清晰:先声明一个实例 yyscan_t scanner;,调用 yylex_init(&scanner) 完成初始化,之后每次调用词法分析都带上这个句柄 yylex(scanner)。同时,建议在文件顶部加 %option noyywrap 去掉对 yywrap 的依赖,减少链接负担;在性能敏感的场合,还可以通过调整缓冲区大小等选项进一步优化吞吐——这些雕琢细节会让你的流水线跑得更顺滑。

至此,我们完整参观了一家现代化的“词法切割工厂”:从入门安装、规格书版式,到识别镜、动作按钮、工作模式,再到多机协同的先进车间。每条流水线都配了自己的控制芯片后,后厨才算真正具备了量产能力。下一步,我们看看这套词法工具在现代软件工程里还能在哪些舞台上发光发热。

\displaystyle \texttt{yyscan\_t}\;s;\quad\texttt{yylex\_init(&s);}\quad\texttt{yylex(s);}
这是可重入扫描器的“三件套”:声明实例句柄、初始化实例、带着实例执行扫描。每个句柄都是独立的流水线,互不串味。
  1. 声明实例\texttt{yyscan\_t}\;s 声明一个扫描器句柄变量,相当于为一条新流水线领取一张独立工牌。
    yyscan_t  s\displaystyle \texttt{yyscan\_t}\;s
  2. 初始化\texttt{yylex\_init(&s)} 完成该实例的初始配置,给流水线通电试运行。
    \displaystyle \texttt{yylex\_init(&s)}
  3. 传参扫描\texttt{yylex(s)} 携带句柄执行扫描,所有状态都写回这个实例内部,不影响其他实例。
    yylex(s)\displaystyle \texttt{yylex(s)}

AI 应用场景

代码编辑器的智能语法高亮

阶段:前端词法分析(文本预处理)

解决的问题:在没有词法分析器的“蛮荒时代”,编辑器要实现语法高亮只能靠整行关键字搜索——结果注释里的 if 也被上色,字符串里的 return 也变了颜色,视觉误导严重。引入词法分析规则后,编辑器先在字符流上识别出“注释”“字符串”“关键字”等不同 Token,再按 Token 类型着色,精准还原代码结构。

具体案例:VS Code 的 TextMate 语法定义本质就是一系列“作用域 + 正则模式”的映射,其底层逻辑与 .l 文件的规则区如出一辙;Sublime Text 的 .sublime-syntax 文件同样使用正则规则对代码分门别类。语言插件开发者每天都在用“识别镜 + 动作按钮”的思路为几十种语言配色。

日志系统的字段结构化

阶段:日志预处理与特征提取

解决的问题:Nginx 的 access.log 一行就有几十个字段,靠字符串 split 容易错位,靠多条正则反复匹配性能又差。用 flex 写一个日志扫描器,一条规则认 IP、一条规则认时间戳、一条规则认状态码,扫描头一次扫过即可把整行日志切成结构化字段,速度快到可以扛住每秒数万行新增日志。

具体案例:某 SRE 团队用 flex 编写了一个轻量日志解析器,把 access.log 实时切分为 JSON 键值对,再送入告警系统做异常检测;同样的思路也常见于 APM 工具的 trace 解析模块。

SQL 注入检测的前置分词

阶段:安全审计中的语法分析前置

解决的问题:检测“堆叠注入”“注释绕过”等攻击手法时,前提是先把一条 SQL 语句切成 Token 流,才能判断关键字顺序是否异常。用 flex 的关键字规则识别 SELECT、UNION,用运算符规则识别 --、# 注释符,再配合开始条件判断当前处于“字符串中”还是“语句边界”,异常结构便无处遁形。

具体案例:数据库防火墙产品在 SQL 进入执行引擎前,先调用内置 lexer 对语句做 Token 化,若发现注释符后紧跟完整语句,即判定为注入尝试并拦截。

Python 演示

python40 行
# -*- coding: utf-8 -*-# 用 PLY 库体验 Lex/Flex 的词法分析流程,先执行: pip install plyfrom ply import lex # 相当于 .l 文件的【定义区】:声明所有 Token 类型tokens = ('NUMBER', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN') # 相当于规则区的【正则模式】t_PLUS = r'\+'t_MINUS = r'-'t_TIMES = r'\*'t_DIVIDE = r'/'t_LPAREN = r'\('t_RPAREN = r'\)'t_ignore = ' \t'  def t_NUMBER(t):    r'\d+'    t.value = int(t.value)    return t  def t_newline(t):    r'\n+'    t.lexer.lineno += len(t.value)  def t_error(t):    print('非法字符: %s' % t.value[0])    t.lexer.skip(1)  lexer = lex.lex() data = '(3 + 12) * 5 - 8 / 2'lexer.input(data) for tok in lexer:    print('%s : %s (第 %d 行)' % (tok.type, tok.value, tok.lineno))
目的:用 PLY 库复刻 Flex 的核心流程——用正则定义 Token 形状、用函数定义动作、最后循环消费 Token 流,让你在纯 Python 环境里直观体会词法分析器“识别-动作-产出”的完整链路。
  1. from ply import lex导入 PLY 的词法器组件,相当于把 flex 工厂的图纸引擎搬进 Python。
  2. tokens = (...)枚举所有 Token 类型名称,相当于 .l 文件定义区里声明“本流水线会产出哪些料”。
  3. t_NUMBER(t)函数名 t_ 前缀告诉 PLY“这是一条词法规则”,函数体是动作,正则在 docstring 里。
  4. t.lexer.skip(1)遇到非法字符时跳过它,对应 Lex 规则区兜底规则 . ; 的作用,防止死循环。
  5. lexer = lex.lex()根据所有 t_ 规则动态生成词法分析器,类似运行 flex 生成 lex.yy.c。
  6. lexer.input(data)输入源文本,相当于把“整卷培根”送上流水线的传送带。
  7. for tok in lexer:不断调用底层 yylex 取出下一个 Token,直到文件末尾返回 0。

常见误区

误区“我在规则文件前面写的规则应该更优先匹配,先写数字规则就应该先匹配数字。”

正解Flex 的匹配策略是“最长匹配优先”——多条规则都能匹配时,选择吞进去文本最长的规则;只有匹配长度完全相同时,才按规则文件中的先后顺序决定。这就像厨师面对食材,永远先按最大块下刀,块头一样大才轮到比较哪把刀更顺手。

为何常见:很多初学者把规则区类比成 switch-case 分支,以为先到先得,忽略了词法工具特有的“最长优先”语义。

误区“我的规则明明写了数字和关键字,为什么扫描结果里全是莫名其妙的字符?”

正解Lex/Flex 不会自动跳过空白。空格、制表符和换行都需要你在规则区显式处理:要么写空动作规则忽略它们,要么在动作中更新行号。若不处理,空白字符会落入兜底规则,导致 Token 流里混入杂音,甚至让错误字符撞进你的业务逻辑。

为何常见:许多高级语言的 split() 会自动去空白,这份“无意识依赖”被直接带到了词法规则编写中。

误区“我写 `a.c` 就是想匹配 a 点 c 三个字母,结果它匹配了 abc。”

正解在正则语法中,. 是元字符,表示“任意一个字符”。a.c 能匹配 abc、a1c、a_c 等等无数种组合。要匹配字面意义的英文句点,必须转义为 a\.c。这就像副厨的识别镜默认把“点”看作“任意食材的通配符”,想让镜片只看真正的句号,得给镜片加上转义滤镜。

为何常见:字符串字面量的直觉与正则元语言的语法规则发生了冲突,这类混淆在正则入门者中几乎必然出现。

误区“我的程序跑起来没完没了,是不是哪里死循环了?”

正解动作里如果没有 return,yylex() 在动作执行后会继续扫描下一个 Token,这是完全正常的设计——它本来就是“切完一片自动切下一片”的循环式流水线。真正需要你用 return 控制的,只是“当前 Token 类型要不要抛给上层语法分析器”这个问题。想要一次只取一个 Token,就在动作里显式 return。

为何常见:把“不 return 就一直切”误当成故障,实际上是没有理解扫描器天然无限供料的模型。

误区“我改好了 `.l` 文件里的规则,直接 `gcc` 重新编译就行了吧?”

正解.l 文件是源规格书,不是 C 源码。Flex 工具的第一步是把规格书翻译成 lex.yy.c,你修改规则后必须先重新运行 flex 生成新的 C 文件,再交给 gcc 编译。跳过 flex 直接编译,编译器看到的还是旧流水线图纸,改动自然石沉大海。

为何常见:习惯了“源码 → 编译器”的单步心智模型,忽略了 Lex/Flex 这种“代码生成器”角色中间多出的翻译环节。

面试题

Q1. flex 命令读取 .l 文件后,默认生成的 C 文件名是什么?

单选题 · easy

  • A.lex.yy.c← 正确答案
  • B.output.c
  • C.scanner.c
  • D.token.c

答案:A。Flex 工厂按规格书施工后,默认交付的源码文件名就是 lex.yy.c。这好比流水线工厂交付的默认机器型号,无需指定就固定叫这个名字。选项中的 output.c、scanner.c、token.c 都不是 flex 的默认产物。

Q2. Lex/Flex 规则文件的扩展名通常是什么?

单选题 · easy

  • A..y
  • B..l← 正确答案
  • C..flex
  • D..lex

答案:B。规则文件约定使用 .l 扩展名,取自 "lexer" 的首字母。.y 是 Yacc/Bison 语法分析器文件的扩展名,.flex 和 .lex 都不是标准约定。记住 .l 就像记住后厨规格书统一用蓝色封皮一样。

Q3. .l 文件中 %% 符号出现几次,能把文件正确分成三段?

单选题 · easy

  • A.1 次
  • B.2 次← 正确答案
  • C.3 次
  • D.4 次

答案:B。两个 %% 像两块分界石,把文件切出定义区、规则区、用户代码区三块区域。出现 1 次只能分出两段,3 次或 4 次则会让 flex 的解析器迷失在错误的分区里,正如三块白板之间只需要两道隔断。

Q4. 以下哪个正则表达式能匹配 C 语言标识符(变量名)?

单选题 · mid

  • A.[0-9]+
  • B.[a-zA-Z_][a-zA-Z0-9_]*← 正确答案
  • C.[a-z]*
  • D..*

答案:B。C 标识符必须以字母或下划线开头,后面可跟任意字母、数字、下划线,所以 B 完全符合。A 只匹配纯数字;C 排除了大写字母和下划线;D 的 .* 会匹配任意字符,包含非法变量名,像一副来者不拒的识别镜,毫无筛选能力。

Q5. 当输入文本可被多条规则同时匹配时,Flex 默认选择哪一条?

单选题 · mid

  • A.最短匹配
  • B.最长匹配← 正确答案
  • C.最先匹配
  • D.最后匹配

答案:B。Flex 采用“最长匹配优先”策略,在同时命中时吞下最多的字符,等长时再按规则出现顺序裁决。这就像厨师切菜时永远优先切最大块——先保住最大收益,再谈先后。

Q6. 在默认(非可重入)模式下,关于 yytext 的说法正确的是?

单选题 · mid

  • A.存放当前匹配到的文本← 正确答案
  • B.存放匹配文本的长度
  • C.是全局变量
  • D.每次匹配后自动释放内存

答案:A。yytext 是字符指针,指向当前匹配到的文本内容,长度另有 yyleng 保存;默认模式下它是全局变量,可重入模式下才改为参数传递。它指向的内存由扫描器管理,不会“自动释放”,因此 D 错误。

Q7. 正则表达式 [0-9]+ 中,+ 的含义是什么?

单选题 · mid

  • A.前一项出现 0 次或 1 次
  • B.前一项出现 0 次或多次
  • C.前一项出现 1 次或多次← 正确答案
  • D.前一项恰好出现 1 次

答案:C。+ 表示“至少一个”,即前一项出现 1 次或多次,所以 [0-9]+ 能匹配 6、60、2024。* 才是 0 次或多次,? 才是 0 次或 1 次。三者就像切菜时“至少来一刀”“切不切都行”“最多来一刀”三种约定的区别。

Q8. 声明了开始条件 COMMENT 但从未调用 BEGIN(COMMENT),会发生什么?

单选题 · hard

  • A.编译直接报错
  • B.COMMENT 限定的规则永远不会匹配← 正确答案
  • C.所有普通规则全部失效
  • D.扫描器自动进入 COMMENT 状态

答案:B。开始条件只是声明了一种可能的工作模式,必须用 BEGIN(COMMENT) 手动切换进入;没切则永远停留在 INITIAL 状态,带 <COMMENT> 前缀的规则自然永不触发。编译不会报错,普通规则也照常工作,只是注释模式成了“从未通电”的备用车间。

Q9. 关于 Flex 可重入扫描器(reentrant),以下哪一个不属于它的行为?

单选题 · hard

  • A.使用 %option reentrant 开启
  • B.状态通过 yyscan_t 句柄传递
  • C.支持在程序中创建多个扫描器实例
  • D.所有状态继续存放在全局变量中← 正确答案

答案:D。可重入扫描器的核心目的就是把状态从全局变量中解放出来,封装进 yyscan_t 句柄。D 描述的是默认非可重入模式的行为,与可重入的设计背道而驰。其余三项都是可重入特性的标准特征,正如多台独立流水线各有各的工牌。

Q10. 修改了 .l 文件规则后,正确的重新构建流程包含哪些步骤?

多选题 · hard

  • A.直接运行 gcc lex.yy.c 编译
  • B.重新执行 flex fruit.l 生成新 lex.yy.c← 正确答案
  • C.用 gcc 编译新生成的 lex.yy.c← 正确答案
  • D.手动修改 lex.yy.c 中的规则

答案:BC。修改规格书后必须重跑 flex 再编译,即 B 和 C 的组合。A 跳过了 flex 翻译步骤,gcc 只能看到旧代码;D 手动改生成的 C 文件既不规范又极易出错——正如改蓝图的正确方式是改原图,而不是拿铅笔涂改工厂交付的机器说明书。