词法分析器的设计目标
我们的词法分析器需要能够处理C语言源代码,并识别以下几类Token:

(图片来源网络,侵删)
- 关键字: 如
int,void,if,else,while,for,return等。 - 标识符: 由字母、数字或下划线组成,且不能以数字开头,如
myVar,count,_temp。 - 常量:
- 整数常量: 如
123,0xFF,075。 - 浮点数常量: 如
14,.5,1e-5。 - 字符常量: 如
'a','\n'。 - 字符串常量: 如
"Hello, World!"。
- 整数常量: 如
- 运算符:
- 算术运算符: , , , , , , 。
- 关系运算符:
>,<,>=,<=, , 。 - 逻辑运算符:
&&, , 。 - 位运算符:
&, ,^, ,<<,>>。 - 赋值运算符: , , , , , 等。
- 分隔符: , , , , , ,
[,]。 - 注释: 单行注释 () 和多行注释 ()。
- 预处理指令: 以 开头的行,如
#include,#define。 - 空白字符: 空格、制表符、换行符,它们通常不生成Token,但用于分隔其他Token。
输出格式: 对于每个识别出的Token,我们输出一行,格式为:<Token类型, Token值>。
设计思路与数据结构
1 Token的定义
我们可以使用一个C语言结构体来表示一个Token。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h> // 用于字符分类函数
// 定义Token类型枚举
typedef enum {
KEYWORD,
IDENTIFIER,
INTEGER,
FLOAT,
CHAR,
STRING,
OPERATOR,
DELIMITER,
PREPROCESSOR,
COMMENT,
UNKNOWN
} TokenType;
// 定义Token结构体
typedef struct {
TokenType type;
char value[100]; // 存储Token的字符串值
} Token;
2 核心算法:有限状态机
词法分析的本质是一个有限状态机,我们的程序将逐个字符地读取源文件,并根据当前字符和所处的状态,决定下一个状态,直到识别出一个完整的Token。
基本流程:

(图片来源网络,侵删)
- 打开源文件。
- 初始化一个状态变量(
state)和用于存储当前Token值的缓冲区。 - 循环读取文件中的每个字符:
- 根据当前字符和
state,更新state并将字符追加到缓冲区。 - 当到达一个可以确定Token类型的终结状态时(遇到一个非数字字符,而缓冲区里是数字),就识别出一个Token。
- 将识别出的Token存入结果列表,并重置
state和缓冲区,继续分析。
- 根据当前字符和
- 处理文件结束标志。
- 关闭文件。
完整代码实现
下面是一个完整的、可运行的C语言词法分析器实现。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// --- 定义部分 ---
#define MAX_TOKEN_LEN 100
#define MAX_TOKENS 1000
typedef enum {
// 状态机状态
START,
IN_IDENTIFIER,
IN_NUMBER,
IN_FLOAT,
IN_OPERATOR,
IN_DELIMITER,
IN_STRING,
IN_CHAR,
IN_SINGLE_LINE_COMMENT,
IN_MULTI_LINE_COMMENT,
IN_PREPROCESSOR,
// Token类型
KEYWORD,
IDENTIFIER,
INTEGER,
FLOAT,
CHAR,
STRING,
OPERATOR,
DELIMITER,
PREPROCESSOR,
COMMENT,
UNKNOWN
} TokenType;
typedef struct {
TokenType type;
char value[MAX_TOKEN_LEN];
} Token;
// --- 全局变量 ---
Token tokens[MAX_TOKENS];
int token_count = 0;
// --- 辅助函数 ---
const char* token_type_to_string(TokenType type) {
switch (type) {
case KEYWORD: return "KEYWORD";
case IDENTIFIER: return "IDENTIFIER";
case INTEGER: return "INTEGER";
case FLOAT: return "FLOAT";
case CHAR: return "CHAR";
case STRING: return "STRING";
case OPERATOR: return "OPERATOR";
case DELIMITER: return "DELIMITER";
case PREPROCESSOR: return "PREPROCESSOR";
case COMMENT: return "COMMENT";
case UNKNOWN: return "UNKNOWN";
default: return "N/A";
}
}
int is_keyword(const char* str) {
const char* keywords[] = {"int", "void", "if", "else", "while", "for", "return", "break", "continue", "char", "float", "double"};
for (int i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) {
if (strcmp(str, keywords[i]) == 0) {
return 1;
}
}
return 0;
}
// --- 词法分析器核心函数 ---
void lexer(FILE* source_file) {
int c;
int state = START;
char buffer[MAX_TOKEN_LEN];
int buffer_index = 0;
while ((c = fgetc(source_file)) != EOF) {
// 跳过空白字符(除了换行符,因为注释可能跨行)
if (isspace(c) && state != IN_SINGLE_LINE_COMMENT && state != IN_MULTI_LINE_COMMENT) {
if (state != START) { // 如果缓冲区不为空,说明一个Token结束了
buffer[buffer_index] = '\0';
tokens[token_count].type = UNKNOWN;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
}
continue;
}
switch (state) {
case START:
if (isalpha(c) || c == '_') {
state = IN_IDENTIFIER;
buffer[buffer_index++] = c;
} else if (isdigit(c)) {
state = IN_NUMBER;
buffer[buffer_index++] = c;
} else if (c == '"') {
state = IN_STRING;
buffer[buffer_index++] = c;
} else if (c == '\'') {
state = IN_CHAR;
buffer[buffer_index++] = c;
} else if (c == '/') {
buffer[buffer_index++] = c;
state = IN_OPERATOR; // 先假设是运算符,再判断
} else if (c == '#') {
state = IN_PREPROCESSOR;
buffer[buffer_index++] = c;
} else if (strchr("+-*%=<>!&|^,;()[]{}", c)) {
state = IN_DELIMITER;
buffer[buffer_index++] = c;
} else {
// 未知字符,直接作为UNKNOWN token
buffer[buffer_index++] = c;
buffer[buffer_index] = '\0';
tokens[token_count].type = UNKNOWN;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
}
break;
case IN_IDENTIFIER:
if (isalnum(c) || c == '_') {
buffer[buffer_index++] = c;
} else {
ungetc(c, source_file); // 将不属于标识符的字符放回流中
buffer[buffer_index] = '\0';
tokens[token_count].type = is_keyword(buffer) ? KEYWORD : IDENTIFIER;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
}
break;
case IN_NUMBER:
if (isdigit(c)) {
buffer[buffer_index++] = c;
} else if (c == '.') {
state = IN_FLOAT;
buffer[buffer_index++] = c;
} else {
ungetc(c, source_file);
buffer[buffer_index] = '\0';
tokens[token_count].type = INTEGER;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
}
break;
case IN_FLOAT:
if (isdigit(c)) {
buffer[buffer_index++] = c;
} else {
ungetc(c, source_file);
buffer[buffer_index] = '\0';
tokens[token_count].type = FLOAT;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
}
break;
case IN_STRING:
buffer[buffer_index++] = c;
if (c == '"') {
buffer[buffer_index] = '\0';
tokens[token_count].type = STRING;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
}
break;
case IN_CHAR:
buffer[buffer_index++] = c;
if (c == '\'') {
buffer[buffer_index] = '\0';
tokens[token_count].type = CHAR;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
}
break;
case IN_OPERATOR:
if (c == '/') { // 处理注释
buffer[buffer_index++] = c;
if (buffer[1] == '/') {
state = IN_SINGLE_LINE_COMMENT;
} else if (buffer[1] == '*') {
state = IN_MULTI_LINE_COMMENT;
} else {
// 不是注释,是除法运算符
buffer[buffer_index] = '\0';
tokens[token_count].type = OPERATOR;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
ungetc(c, source_file); // 把第二个'/'放回去
}
} else if (c == '=' || c == '&' || c == '|' || c == '+' || c == '-' || c == '!' || c == '<' || c == '>') {
buffer[buffer_index++] = c;
buffer[buffer_index] = '\0';
tokens[token_count].type = OPERATOR;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
} else {
ungetc(c, source_file);
buffer[buffer_index] = '\0';
tokens[token_count].type = OPERATOR;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
}
break;
case IN_SINGLE_LINE_COMMENT:
buffer[buffer_index++] = c;
if (c == '\n') {
buffer[buffer_index] = '\0';
tokens[token_count].type = COMMENT;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
}
break;
case IN_MULTI_LINE_COMMENT:
buffer[buffer_index++] = c;
if (c == '*') {
int next_c = fgetc(source_file);
buffer[buffer_index++] = next_c;
if (next_c == '/') {
buffer[buffer_index] = '\0';
tokens[token_count].type = COMMENT;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
} else {
ungetc(next_c, source_file); // 如果不是'/',放回去
}
}
break;
case IN_PREPROCESSOR:
buffer[buffer_index++] = c;
if (c == '\n') {
buffer[buffer_index] = '\0';
tokens[token_count].type = PREPROCESSOR;
strcpy(tokens[token_count].value, buffer);
token_count++;
buffer_index = 0;
state = START;
}
break;
}
}
// 处理文件末尾最后一个未完成的Token
if (buffer_index > 0) {
buffer[buffer_index] = '\0';
tokens[token_count].type = UNKNOWN;
strcpy(tokens[token_count].value, buffer);
token_count++;
}
}
// --- 主函数 ---
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "用法: %s <源文件.c>\n", argv[0]);
return 1;
}
FILE* source_file = fopen(argv[1], "r");
if (source_file == NULL) {
perror("无法打开文件");
return 1;
}
printf("开始分析文件: %s\n\n", argv[1]);
lexer(source_file);
fclose(source_file);
printf("词法分析完成,共识别出 %d 个Token:\n", token_count);
printf("--------------------------------------------\n");
for (int i = 0; i < token_count; i++) {
printf("<%s, \"%s\">\n", token_type_to_string(tokens[i].type), tokens[i].value);
}
return 0;
}
如何编译和运行
-
保存代码: 将上面的代码保存为
lexer.c。 -
创建测试文件: 创建一个简单的C语言源文件,
test.c如下:#include <stdio.h> int main() { // 这是一个单行注释 int a = 10; float b = 3.14; char c = 'A'; /* 这是一个 多行注释 */ printf("Hello, %s!\n", "World"); if (a > 5) { return 1; } else { return 0; } } -
编译: 使用GCC编译器编译
lexer.c。
(图片来源网络,侵删)gcc lexer.c -o lexer
-
运行: 执行生成的可执行文件,并将
test.c作为参数传入。./lexer test.c
预期输出
运行后,你将看到类似下面的输出(顺序可能略有不同,取决于状态机的具体实现细节):
开始分析文件: test.c
词法分析完成,共识别出 31 个Token:
--------------------------------------------
<PREPROCESSOR, "#include <stdio.h>">
<DELIMITER, "(">
<DELIMITER, ")">
<DELIMITER, "{">
<COMMENT, "// 这是一个单行注释\n">
<KEYWORD, "int">
<IDENTIFIER, "a">
<OPERATOR, "=">
<INTEGER, "10">
<DELIMITER, ";">
<KEYWORD, "float">
<IDENTIFIER, "b">
<OPERATOR, "=">
<FLOAT, "3.14">
<DELIMITER, ";">
<KEYWORD, "char">
<IDENTIFIER, "c">
<OPERATOR, "=">
<CHAR, "'A'">
<DELIMITER, ";">
<COMMENT, "/* 这是一个\n 多行注释 */">
<IDENTIFIER, "printf">
<DELIMITER, "(">
<STRING, ""Hello, %s!\n"">
<DELIMITER, ",">
<STRING, ""World"">
<DELIMITER, ")">
<DELIMITER, ";">
<KEYWORD, "if">
<DELIMITER, "(">
<IDENTIFIER, "a">
<OPERATOR, ">">
<INTEGER, "5">
<DELIMITER, ")">
<DELIMITER, "{">
<KEYWORD, "return">
<INTEGER, "1">
<DELIMITER, ";">
<DELIMITER, "}">
<KEYWORD, "else">
<DELIMITER, "{">
<KEYWORD, "return">
<INTEGER, "0">
<DELIMITER, ";">
<DELIMITER, "}">
<DELIMITER, "}">
设计要点与局限性分析
优点
- 结构清晰: 使用有限状态机,逻辑结构清晰,易于理解和扩展。
- 功能完整: 覆盖了C语言中大部分常见的词法单元。
- 健壮性: 使用
ungetc将不属于当前Token的字符“退还”给输入流,是处理这类问题的标准做法。
局限性与可扩展性
- 错误处理: 这个简单的词法分析器对错误的处理能力很弱,一个未闭合的字符串 (
"hello) 会导致程序一直读取到文件末尾,并可能产生大量错误,一个更完善的词法分析器应该在遇到非法字符或无法识别的结构时,报告错误并尝试恢复(跳过到下一个Token开始的位置)。 - 浮点数和整数: 当前实现无法处理科学计数法(如
1e5)或十六进制/八进制整数(如0xFF,075),这可以通过扩展IN_NUMBER状态来实现。 - 预处理指令: 当前只将 开头的整行识别为
PREPROCESSOR,没有对#include、#define等进行更细粒度的分析。 - 运算符: 运算符的处理可以更精确。
>>和>是不同的运算符,当前实现可以处理,但逻辑可以进一步优化。 - 性能: 对于非常大的文件,使用
fgetc逐个字符读取效率较低,可以改用fread进行块读取,以提高性能。
这个项目是学习编译原理的一个绝佳实践,通过亲手实现一个词法分析器,你可以深刻理解源代码是如何被计算机逐步解析的,为后续学习语法分析打下坚实的基础。
