calc1 --- 中缀表达式词法分析器
1. 项目概述
构建一个 C 语言中缀表达式词法分析器(tokenizer),将中缀表达式字符串拆分为独立的 token 序列,包括数字、四则运算符和圆括号。
输入示例:
(90-18)/3+15
输出示例:
( LEFT_PAREN
90 NUMBER
- MINUS
18 NUMBER
) RIGHT_PAREN
/ SLASH
3 NUMBER
+ PLUS
15 NUMBER
2. 目录结构
calc/
├── Makefile # 构建脚本(gcc -Wall -Wextra -std=c99 -pedantic)
├── parse.h # 头文件:TokenType 枚举、Token 结构体、API 声明
├── parse.c # 词法分析器实现
├── main.c # 入口程序:输入源选择 + 解析打印
└── build/
├── calc # 可执行文件
├── parse.o # 编译目标文件
└── main.o # 编译目标文件
3. 文件说明
3.1 parse.h
定义三个核心类型和三个 API 函数:
TokenType 枚举:
| 枚举值 | 对应字符 | 说明 |
|---|---|---|
TOKEN_NUMBER |
--- | 整数字面量 |
TOKEN_PLUS |
+ |
加号 |
TOKEN_MINUS |
- |
减号 |
TOKEN_STAR |
* |
乘号 |
TOKEN_SLASH |
/ |
除号 |
TOKEN_LPAREN |
( |
左括号 |
TOKEN_RPAREN |
) |
右括号 |
TOKEN_END |
--- | 输入结束 |
TOKEN_ERROR |
--- | 非法字符 |
Token 结构体:
c
typedef struct {
TokenType type; // token 类型
int value; // 数值(仅 TOKEN_NUMBER 有效)
int col; // 在输入字符串中的列号(1-based)
} Token;
API 函数:
c
void parse_init(const char *input); // 初始化解析器
Token parse_next(void); // 读取下一个 token
const char *token_name(TokenType type); // 将类型转为可读字符串
3.2 parse.c
词法分析器实现,采用静态状态机模式:
- 内部状态 :
input_pos(当前扫描游标)、line(行号,保留)、column(列号,1-based) parse_init:将游标指向输入字符串起始位置,重置行列号parse_next:核心逻辑- 跳过空白字符(
isspace),同步更新列号 - 遇到
\0→ 返回TOKEN_END - 遇到
0-9→ 用strtol连续读入数字,返回TOKEN_NUMBER - 遇到
+-*/()→ 返回对应类型 - 其他字符 → 返回
TOKEN_ERROR
- 跳过空白字符(
token_name:将枚举值映射为可读字符串
设计要点:零动态内存分配,纯游标扫描,无需 malloc。
3.3 main.c
入口程序,分为两个函数:
parse_and_print(static 函数):
c
static int parse_and_print(const char *input);
职责:空输入校验 → parse_init → 循环调用 parse_next 打印每个 token → 遇到 TOKEN_ERROR 时输出错误位置并返回 1。
main 函数:
c
int main(int argc, char *argv[]);
职责:输入源选择(argv[1] 优先,否则从 stdin 读取)→ 调用 parse_and_print 并返回其状态码。
3.4 Makefile
沿用项目中 nslist/ 的构建风格(C99 严格模式):
| 目标 | 作用 |
|---|---|
all |
编译生成 build/calc 可执行文件 |
run |
运行 build/calc,需传入 EXPR=... |
clean |
删除 build/ 目录 |
编译选项:-Wall -Wextra -std=c99 -pedantic -g -O2
4. 构建与运行
bash
# 编译
$ cd calc
$ make
mkdir -p build
gcc -Wall -Wextra -std=c99 -pedantic -g -O2 -I. -c parse.c -o build/parse.o
gcc -Wall -Wextra -std=c99 -pedantic -g -O2 -I. -c main.c -o build/main.o
gcc -Wall -Wextra -std=c99 -pedantic -g -O2 build/parse.o build/main.o -o build/calc
# 命令行参数运行
$ ./build/calc "(90-18)/3+15"
# 管道输入运行
$ echo "(90-18)/3+15" | ./build/calc
# 通过 Makefile 运行
$ make run EXPR="(90-18)/3+15"
# 清理
$ make clean
5. 测试验证
5.1 基本表达式
bash
$ ./build/calc "(90-18)/3+15"
( LEFT_PAREN
90 NUMBER
- MINUS
18 NUMBER
) RIGHT_PAREN
/ SLASH
3 NUMBER
+ PLUS
15 NUMBER
5.2 带空格
bash
$ ./build/calc " 3 + 5 "
3 NUMBER
+ PLUS
5 NUMBER
5.3 非法字符
bash
$ ./build/calc "3+@5"
3 NUMBER
+ PLUS
col 3: ERROR --- illegal character
5.4 空输入
bash
$ ./build/calc ""
error: empty input
5.5 管道输入
bash
$ echo "10-2" | ./build/calc
10 NUMBER
- MINUS
2 NUMBER
6. 设计要点
- 零动态内存分配 :词法分析器使用纯游标扫描,不依赖
malloc,适合嵌入式或资源受限环境 - C99 严格模式 :编译启用
-std=c99 -pedantic,确保跨平台兼容 - 列号定位:每个 token 记录输入中的列号,支持精确的错误位置报告
- 模块分离 :
parse.h/parse.c为纯词法分析模块,main.c仅负责输入输出编排,职责清晰 - 两种输入方式:支持命令行参数和标准输入管道,方便脚本集成