c语言词法分析程序设计

99ANYc3cd6
预计阅读时长 56 分钟
位置: 首页 C语言 正文

词法分析器的设计目标

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

c语言词法分析程序设计
(图片来源网络,侵删)
  1. 关键字: 如 int, void, if, else, while, for, return 等。
  2. 标识符: 由字母、数字或下划线组成,且不能以数字开头,如 myVar, count, _temp
  3. 常量:
    • 整数常量: 如 123, 0xFF, 075
    • 浮点数常量: 如 14, .5, 1e-5
    • 字符常量: 如 'a', '\n'
    • 字符串常量: 如 "Hello, World!"
  4. 运算符:
    • 算术运算符: , , , , , , 。
    • 关系运算符: >, <, >=, <=, , 。
    • 逻辑运算符: &&, , 。
    • 位运算符: &, , ^, , <<, >>
    • 赋值运算符: , , , , , 等。
  5. 分隔符: , , , , , , [, ]
  6. 注释: 单行注释 () 和多行注释 ()。
  7. 预处理指令: 以 开头的行,如 #include, #define
  8. 空白字符: 空格、制表符、换行符,它们通常不生成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。

基本流程:

c语言词法分析程序设计
(图片来源网络,侵删)
  1. 打开源文件。
  2. 初始化一个状态变量(state)和用于存储当前Token值的缓冲区。
  3. 循环读取文件中的每个字符:
    • 根据当前字符和 state,更新 state 并将字符追加到缓冲区。
    • 当到达一个可以确定Token类型的终结状态时(遇到一个非数字字符,而缓冲区里是数字),就识别出一个Token。
    • 将识别出的Token存入结果列表,并重置 state 和缓冲区,继续分析。
  4. 处理文件结束标志。
  5. 关闭文件。

完整代码实现

下面是一个完整的、可运行的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;
}

如何编译和运行

  1. 保存代码: 将上面的代码保存为 lexer.c

  2. 创建测试文件: 创建一个简单的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;
        }
    }
  3. 编译: 使用GCC编译器编译 lexer.c

    c语言词法分析程序设计
    (图片来源网络,侵删)
    gcc lexer.c -o lexer
  4. 运行: 执行生成的可执行文件,并将 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的字符“退还”给输入流,是处理这类问题的标准做法。

局限性与可扩展性

  1. 错误处理: 这个简单的词法分析器对错误的处理能力很弱,一个未闭合的字符串 ("hello) 会导致程序一直读取到文件末尾,并可能产生大量错误,一个更完善的词法分析器应该在遇到非法字符或无法识别的结构时,报告错误并尝试恢复(跳过到下一个Token开始的位置)。
  2. 浮点数和整数: 当前实现无法处理科学计数法(如 1e5)或十六进制/八进制整数(如 0xFF, 075),这可以通过扩展 IN_NUMBER 状态来实现。
  3. 预处理指令: 当前只将 开头的整行识别为 PREPROCESSOR,没有对 #include#define 等进行更细粒度的分析。
  4. 运算符: 运算符的处理可以更精确。>>> 是不同的运算符,当前实现可以处理,但逻辑可以进一步优化。
  5. 性能: 对于非常大的文件,使用 fgetc 逐个字符读取效率较低,可以改用 fread 进行块读取,以提高性能。

这个项目是学习编译原理的一个绝佳实践,通过亲手实现一个词法分析器,你可以深刻理解源代码是如何被计算机逐步解析的,为后续学习语法分析打下坚实的基础。

-- 展开阅读全文 --
头像
dede文章跳转属性如何设置与使用?
« 上一篇 今天
C语言开发工具,到底哪个好?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]