【编译原理实验一】——词法分析器(基于自动机的词法分析器的设计与实现)

本篇适用于ZZU的编译原理课程实验一——基于自动机的词法分析器的设计与实现,包含了实验代码实验报告的内容,读者可根据需要参考完成自己的程序设计。
如果是ZZU的学弟学妹看到这篇,那么恭喜你,你来对地方啦!
如果需要相关文档资料的话,我也已经上传,自取:「编译原理一实验代码+实验报告(ZZU)

不要忘了点赞👍和收藏💌支持一下哦!

源代码

先给出实验的源代码

#include <iostream>
#include <fstream>
#include <string>
#include <utility>
#include <vector>
#include <map>
#include <cctype> // 提供一系列用于字符分类和转换的函数
using namespace std;

// Token结构体
struct Token {
    int syn;            // 种别码(用户自定义标识符syn为0,数值类型syn为25)
    string token;       // 符号
    int num;            // 数值(-1表示非数值类型)
    
    Token(int s, string t, int n) : syn(s), token(std::move(t)), num(n) {}
};

// 词法分析类
class LexicalAnalyzer {
private:
    // 关键字映射表
    map<string, int> keywords = {
        {"main", 1}, {"if", 2}, {"then", 3}, {"while", 4}, {"do", 5},
        {"static", 6}, {"int", 7}, {"double", 8}, {"struct", 9}, {"else", 10},
        {"long", 11},{"switch", 12}, {"case", 13},{"typedef", 14}, {"char", 15},
        {"return", 16}, {"const", 17}, {"float", 18}, {"break", 19}, {"short", 20},
        {"sizeof", 21}, {"for", 22}, {"void", 23}, {"continue", 24}
    };
    
    // 运算符和分隔符映射表
    map<string, int> operators = {
        {"<=", 26}, {">", 27}, {">=", 28}, {"=", 29}, 
        {"[", 30}, {"]", 31}, {";", 32}, {"(", 33}, {")", 34},
        {"+", 35}, {"-", 36}, {"*", 37}, {"/", 38}, {"**", 39},
        {"==", 40}, {"<", 41}, {"<>", 42}, {",", 43}, {":", 44},
        {"{", 45}, {"}", 46}, {"!=", 47}
    };
    
    string input;       // 输入缓冲区
    size_t pos;        // 当前处理位置
    
    // 判断是否为字母
    static bool isLetter(char c) {
        return isalpha(c) || c == '_';
    }
    
    // 跳过空白字符
    void skipWhitespace() {
        while (pos < input.length() && isspace(input[pos])) {
            pos++;
        }
    }
    
    // 获取运算符或分隔符
    string getOperator() {
        string op;
        char c = input[pos];
        
        // 检查双字符运算符
        if (pos + 1 < input.length()) {
            string twoChar = input.substr(pos, 2);
            if (operators.find(twoChar) != operators.end()) {
                pos += 2;
                return twoChar;
            }
        }

        // 单字符运算符
        ++pos;
        op = c;
        return op;
    }

public:
    LexicalAnalyzer() : pos(0) {}

    // 输出关键字、运算符和分隔符映射表
    void printMappings() const {
        cout << "关键字映射表:" << endl;
        for (const auto& [keyword, code] : keywords) {
            cout << keyword << " --> " << code << endl;
        }
        cout << "\n运算符和分隔符映射表:" << endl;
        for (const auto& [op, code] : operators) {
            cout << op << " --> " << code << endl;
        }
        cout << endl;
    }

    // 对输入文件进行预处理
    bool preprocessFile(const string& filename) {
        ifstream file(filename);
        if (!file.is_open()) {
            cout << "无法打开输入文件:" << filename << endl;
            return false;
        }

        string line;
        string processed;
        bool lastWasSpace = false;   // 标志最近的字符是不是空格
        bool inBlockComment = false; // 标志多行注释

        // 以行为单位读取文件内容
        while (getline(file, line)) {
            size_t i = 0;
            // 对每一行的字符进行分析
            while (i < line.length()) {
                // 检查单行注释
                if (!inBlockComment && line[i] == '/' && i + 1 < line.length() && line[i + 1] == '/') {
                    break;  // 跳过这一行后面的内容
                }

                // 检查多行注释的开始
                if (!inBlockComment && line[i] == '/' && i + 1 < line.length() && line[i + 1] == '*') {
                    inBlockComment = true;
                    i += 2;
                    continue;
                }

                // 检查多行注释的结束
                if (inBlockComment && line[i] == '*' && i + 1 < line.length() && line[i + 1] == '/') {
                    inBlockComment = false;
                    i += 2;
                    continue;
                }

                // 跳过多行注释中的内容
                if (inBlockComment) {
                    i++;
                    continue;
                }

                // 正常处理字符
                char c = line[i++];
                if (isspace(c)) {
                    if (!lastWasSpace) {
                        processed += ' ';
                        lastWasSpace = true;
                    }
                } else {
                    processed += c;
                    lastWasSpace = false;
                }
            }
        }

        input = processed;  // 将预处理好的字符串作为下一步进行词法分析的输入

        // 保存预处理后的文件
        ofstream outFile("preprocessed.txt");
        if (!outFile.is_open()) {
            cout << "无法创建预处理文件" << endl;
            return false;
        }
        outFile << input;
        outFile.close();

        cout << "预处理完成,结果已保存到 preprocessed.txt" << endl;
        return true;
    }

    // 主要词法分析函数
    vector<Token> analyze() {
        vector<Token> tokens;
        pos = 0;

        while (pos < input.length()) {
            skipWhitespace();
            if (pos >= input.length()) break;

            char c = input[pos];

            // 识别数字
            if (isdigit(c)) {
                string num;
                while (pos < input.length() && isdigit(input[pos])) {
                    num += input[pos++];
                }
                int value = stoi(num);  // 字符串转换成整数值
                tokens.emplace_back(25, num, value);
            }

            // 识别标识符和关键字
            else if (isLetter(c)) {
                string word;
                while (pos < input.length() && (isLetter(input[pos]) || isdigit(input[pos]))) {
                    word += input[pos++];
                }

                // 检查是否是关键字
                if (keywords.find(word) != keywords.end()) {
                    tokens.emplace_back(keywords[word], word, -1);
                } else {
                    // 是标识符
                    tokens.emplace_back(0, word, -1);
                }
            }
            // 识别运算符和分隔符
            else {
                string op = getOperator();
                if (operators.find(op) != operators.end()) {
                    tokens.emplace_back(operators[op], op, -1);
                } else if (!isspace(op[0])) {
                    cout << "未识别的字符: " << op << endl;
                    pos++;
                }
            }
        }

        return tokens;
    }
};

int main() {
    string inputFile;
    // 输入文件名
    cout << "请输入要进行分析的文件名:";
    cin >> inputFile;

    LexicalAnalyzer analyzer;

    // 打印关键字、运算符和分隔符映射表
    analyzer.printMappings();

    // 预处理文件
    if (!analyzer.preprocessFile(inputFile)) {
        return 1;
    }

    // 进行词法分析
    vector<Token> tokens = analyzer.analyze();

    // 输出结果
    cout << "\n词法分析结果:" << endl;
    for (const Token& t : tokens) {
        cout << "{TypeNum(syn): " << t.syn
             << ", TokenLiteral(token): " << t.token
             << ", Num: " << t.num << "}" << endl;
    }

    return 0;
}


实验报告

接下来是实验报告的内容,希望能帮助读者理解词法分析程序的设计思路,以及完成实验报告的撰写


一.实验目的

设计与实现一个词法分析器,加深对词法分析原理的理解。通过本实验,掌握词法分析器的基本功能和实现方法,能够根据给定的语言规则识别源程序中的单词符号,并将其转换为相应的种别码表示,为后续的语法分析等编译阶段提供基础支持。

二.问题描述

1.实验内容
需要实现的功能:
1)输入:源程序字符串,源程序存储在文本文件中(编码格式ANSI),文件名作为命令行参数输入;
2)输出:输出token序列到标准输出设备。

2.实验要求
2.1 语言的词法
1、关键字

   main
   if then else
   while do
   repeat until
   for from to step
   switch of case default
   return
   integer real char bool
   and or not mod 
   read write

所有关键字都是小写。
2、专用符号
运算符包括:=、+、-、、/、<、<=、>、>=、!=
分隔符包括:,、;、:,{、}、[、]、(、)
3、其它标记ID和NUM
通过以下正规式定义其它标记:
ID letter(letter | digit)

NUM digit digit*
Letter a | … | z | A | … | Z
Digit 0|…|9
4、空白格由空格、制表符和换行符组成
空白一般用来分隔ID、NUM、专用符号和关键字,词法分析阶段通常被忽略。
2.2 单词符号种别码
根据实验要求以及自己的实际情况,设置的种别码对照表如下:

表1-1 种别码对照表

单词种别码单词种别码单词种别码单词种别码
main1else10break19>=28
if2long11short20=29
then3switch12sizeof21[30
while4case13for22]31
do5typedef14void23;32
static6char15continue24(33
int7return16#25)34
double8const17<=26+35
struct9float18>27-36
*37/38**39==40
<41<>42!=47:44
,43{45}46

2.3设计自动机
本实验设计了一个有限自动机来描述该语言的词法。自动机从初始状态开始,根据输入字符的类型进行状态转移,当到达终止状态时,识别出一个单词符号,并确定其种别码。例如,当遇到字母时,自动机进入识别标识符或关键字的状态;遇到数字时,进入识别数字的状态;遇到特定的运算符或分隔符时,直接转移到相应的终止状态。

三.软件设计方法的选择

1.软件设计方法
本次实验中,我使用了面向对象的软件设计方法,主要设计了LexicalAnalyzer类作为词法分析器的核心类。

2.各阶段的模型
2.2 分析阶段
(1)对象模型(领域模型):在本实验中,设计了LexicalAnalyzer类作为词法分析器的核心类,包含了关键字映射表、运算符和分隔符映射表、输入缓冲区、当前处理位置等属性,以及用于词法分析的各种方法。同时,还定义了Token结构体来表示识别出的单词符号,包含种别码、符号和数值等信息。这些类和结构体之间的关系体现了系统的静态数据结构。

(2)用例模型:行为者为用户,用户的主要用例是输入源程序文件名,系统完成对源程序的预处理、词法分析,并输出 token 序列。

(3)活动图:总体业务流程为用户输入文件名后,系统先对文件进行预处理,去除注释和多余空白,然后进行词法分析,将源程序中的字符流转换为 token 序列输出。

(4)交互图:在词法分析过程中,LexicalAnalyzer类的各个方法之间相互协作,如preprocessFile方法与analyze方法之间通过共享输入缓冲区input进行交互,analyze方法内部又调用了skipWhitespace、getOperator等方法来实现对不同类型字符的处理。

2.2 设计阶段
(1)对象模型(类图):LexicalAnalyzer类与Token结构体之间存在关联,LexicalAnalyzer类负责生成Token对象。LexicalAnalyzer类包含多个私有成员变量用于存储词法分析相关的数据,以及多个公有方法用于实现词法分析的功能,如printMappings、preprocessFile、analyze等。

(2)主要交互图:在main函数中创建LexicalAnalyzer对象,然后依次调用其printMappings、preprocessFile和analyze方法,analyze方法内部根据字符类型调用不同的辅助方法进行处理,最终生成Token对象并存储在向量中返回。

3.开发语言和开发环境
开发语言:C++(使用 ISO C++11 标准)。
开发环境:使用 g++ 编译器,允许使用 STL。

四.分析模型

(1)对象模型(领域模型):在本实验中,设计了LexicalAnalyzer类作为词法分析器的核心类,包含了关键字映射表、运算符和分隔符映射表、输入缓冲区、当前处理位置等属性,以及用于词法分析的各种方法。同时,还定义了Token结构体来表示识别出的单词符号,包含种别码、符号和数值等信息。这些类和结构体之间的关系体现了系统的静态数据结构。

类图说明:
1)Token类:
属性:syn(种别码)、token(符号)、num(数值)
构造函数:Token(int s, string t, int n)

2)LexicalAnalyzer类:
私有属性:
keywords:关键字映射表
operators:运算符和分隔符映射表
input:输入缓冲区
pos:当前处理位置
私有方法:
isLetter():判断是否为字母
skipWhitespace():跳过空白字符
getOperator():获取运算符或分隔符
公有方法:
构造函数
printMappings():打印映射表
preprocessFile():预处理文件
analyze():进行词法分析

(2)用例模型:行为者为用户,用户的主要用例是输入源程序文件名,系统完成对源程序的预处理、词法分析,并输出 token 序列。
在这里插入图片描述
用例图说明:
1)主要角色:
用户:系统的使用者

2)主要用例:
查看映射表
输入源文件
进行词法分析

3)子用例:
预处理文件
移除注释
处理空白字符

4)词法分析过程
识别标识符
识别关键字
识别数字
识别运算符
识别分隔符
生成Token
输出分析结果

(3)活动图:总体业务流程为用户输入文件名后,系统先对文件进行预处理,去除注释和多余空白,然后进行词法分析,将源程序中的字符流转换为 token 序列输出。

  • 预处理阶段活动图
    在这里插入图片描述
  • 词法分析阶段活动图
    在这里插入图片描述
    1)预处理阶段活动图说明:
    主要任务:清理源代码中的注释和规范化空白字符
    核心流程:
    文件读取:逐行处理输入文件
    注释处理:处理单行注释(//)和多行注释(/* */)
    空白处理:将连续空白字符压缩为单个空格
    结果保存:保存处理后的清理文本

2)词法分析阶段活动图说明:
主要任务:将字符流转换为Token序列
核心流程:
字符分类:根据首字符确定处理方向
标识符处理:识别标识符并查找关键字表
数字处理:收集并转换数字序列
运算符处理:识别单字符和双字符运算符
Token生成:创建对应类型的Token并添加到结果序列

(4)交互图:在词法分析过程中,LexicalAnalyzer类的各个方法之间相互协作,如preprocessFile方法与analyze方法之间通过共享输入缓冲区input进行交互,analyze方法内部又调用了skipWhitespace、getOperator等方法来实现对不同类型字符的处理。
在这里插入图片描述
该序列图展示了词法分析器中各个对象之间的交互过程,主要包括以下几个部分:
1)初始化阶段:
用户输入文件名
创建词法分析器实例
显示关键字和运算符映射表
2)预处理阶段:
打开源文件
逐行读取并处理
处理注释和空白字符
保存预处理结果

3)词法分析阶段:
跳过空白字符
根据字符类型进行不同处理:
标识符处理:收集字符并查找关键字表
数字处理:收集并创建数值Token
运算符处理:识别并创建运算符Token

4)结果输出阶段:
返回Token序列
显示分析结果

5)主要参与者说明:
用户(User):系统的使用者
主程序(Main):程序的入口点,协调整个过程
词法分析器(LexicalAnalyzer):核心处理类
文件系统(File):处理文件读写
映射表(Map):存储关键字和运算符的映射关系
Token对象:表示识别出的词法单元

6)交互效果分析:
清晰的层次结构:用户 -> 主程序 -> 分析器 -> 具体处理
完整的处理流程:从文件输入到结果输出
详细的条件分支:不同类型的Token处理流程
错误处理机制:包含了匹配失败的处理

五.设计模型

(1)对象模型(类图):LexicalAnalyzer类与Token结构体之间存在关联,LexicalAnalyzer类负责生成Token对象。LexicalAnalyzer类包含多个私有成员变量用于存储词法分析相关的数据,以及多个公有方法用于实现词法分析的功能,如printMappings、preprocessFile、analyze等。
(2)主要交互图:在main函数中创建LexicalAnalyzer对象,然后依次调用其printMappings、preprocessFile和analyze方法,analyze方法内部根据字符类型调用不同的辅助方法进行处理,最终生成Token对象并存储在向量中返回。

六.主要算法描述

1.预处理算法
1)预处理实现流程描述
开始,打开输入文件。
逐行读取文件内容,对于每一行:
检查是否为单行注释,如果是,跳过该行剩余内容。
检查是否为多行注释开始,如果是,标记进入多行注释状态,跳过注释起始符号。
检查是否为多行注释结束,如果是,标记退出多行注释状态,跳过注释结束符号。
如果处于多行注释中,跳过当前字符。
否则,正常处理字符,判断是否为空白字符,如果是且前一个字符不是空白字符,则添加一个空格到预处理字符串中;如果不是空白字符,则直接添加到预处理字符串中。
关闭输入文件,将预处理后的字符串作为输入缓冲区,打开输出文件,将预处理字符串写入输出文件,关闭输出文件。

2)预处理算法流程图
在这里插入图片描述
3)预处理算法伪代码:

PreprocessFile(filename):
    初始化 processed = ""  // 存储处理后的文本
    初始化 inBlockComment = false  // 多行注释标记
    初始化 lastWasSpace = false   // 空格标记
    对文件中的每一行 line:
        i = 0
        while i < line.length:
            if not inBlockComment and 发现"//" :
                跳过当前行
                break
            
            if not inBlockComment and 发现"/*" :
                inBlockComment = true
                i += 2
                continue
            
            if inBlockComment and 发现"*/" :
                inBlockComment = false
                i += 2
                continue
            
            if inBlockComment:
                i++
                continue
                
            if 当前字符是空白:
                if not lastWasSpace:
                    processed += ' '
                    lastWasSpace = true
            else:
                processed += 当前字符
                lastWasSpace = false
            i++ 
    return processed

2.词法分析算法
1)词法分析算法描述
初始化阶段:创建空的Token序列用于存储结果,开始读取字符。
主循环处理:检查是否到达输入结尾,跳过空白字符,根据当前字符类型分流处理。

三种主要的处理分支:
数字处理流程:收集连续数字序列,转换为整数值,生成数值Token。
标识符处理流程:收集标识符字符串,查找是否为关键字,生成对应类型Token。
运算符处理流程:尝试匹配双字符运算符,尝试匹配单字符运算符,处理非法字符错误。

循环控制:每生成一个Token后返回读取下一个字符,直到处理完所有输入。
结束处理:返回完整的Token序列,结束分析过程。

2)词法分析算法流程图
在这里插入图片描述
3)词法分析算法伪代码

函数 analyze() -> 返回值:词法单元列表(Token List)
  begin
    初始化词法单元列表 tokens
    设置当前处理位置 pos 为 0

    当 pos 小于 输入缓冲区长度 时,重复执行:
        调用 skipWhitespace() 跳过空白字符
        如果 pos >= 输入缓冲区长度,退出循环

        读取当前字符 c = 输入缓冲区[pos]

        // 识别数字
        如果 c 是数字:
            初始化字符串 num
            当 pos 小于 输入缓冲区长度 且 输入缓冲区[pos] 是数字:
                将当前字符追加到 num
                pos 自增 1
            将 num 转换为整数 value
            创建一个新的词法单元(种别码为 25,对应数值类型),内容为 (25, num, value)
            将词法单元加入 tokens 列表
            继续下一次循环

        // 识别标识符和关键字
        如果 c 是字母或下划线:
            初始化字符串 word
            当 pos 小于 输入缓冲区长度 且 输入缓冲区[pos] 是字母、数字或下划线:
                将当前字符追加到 word
                pos 自增 1

            如果 word 在关键字表中:
                创建一个新的词法单元(种别码为关键字对应值),内容为 (关键字种别码, word, -1)
            否则:
                创建一个新的词法单元(种别码为 0,表示标识符),内容为 (0, word, -1)
            将词法单元加入 tokens 列表
            继续下一次循环

        // 识别运算符和分隔符
        获取当前字符对应的运算符 op = getOperator()
        如果 op 在运算符和分隔符表中:
            创建一个新的词法单元(种别码为运算符对应值),内容为 (运算符种别码, op, -1)
            将词法单元加入 tokens 列表
        否则如果 op 不是空白字符:
            输出错误信息 "未识别的字符:op"
            pos 自增 1

    返回 tokens 列表
end

函数 skipWhitespace():
    当 pos 小于 输入缓冲区长度 且 输入缓冲区[pos] 是空白字符:
        pos 自增 1
结束 skipWhitespace 函数


函数 getOperator() -> 返回值:运算符字符串
    初始化字符串 op
    获取当前字符 c = 输入缓冲区[pos]

    如果 pos + 1 小于 输入缓冲区长度:
        检查是否存在双字符运算符
        如果 当前字符 c 与 下一字符组合的双字符运算符在运算符表中:
            pos 自增 2
            返回该双字符运算符
    pos 自增 1
    返回单字符运算符 op
结束 getOperator 函数

七.测试数据与测试效果

1.测试数据格式
测试数据格式:包含各种关键字、运算符、分隔符、标识符和数字的源程序文件。

2.测试样例与测试结果
(1)测试样例1
测试数据:

/*
这一个测试样例中全是注释
这里是多行注释1
这里是多行注释2
*/

// 单行注释1
// 单行注释2

测试结果:
1)预处理文件内容为空:因为测试数据都是注释,与预期的结果一致。
2)控制台输出:没有Token输出,与预期结果一致。
在这里插入图片描述
在这里插入图片描述
(2)测试样例2
测试数据:

多行注释
 */
int main() {
    int num = 20; // 单行注释
    while (num > 0) {
        num = num - 1;
    }
    return num;
}

测试结果:
1)预处理文件内容为删去注释、换行符和多余空格后的源代码:与预期的结果一致。
2)控制台输出:输出词法分析出的Token,与预期结果一致。
在这里插入图片描述
在这里插入图片描述

八.实验总结

(一)实验时间安排
准备时间:花费一定时间理解实验要求,熟悉词法分析原理和自动机相关知识,设计整体框架和数据结构。
上机时间:主要用于编写代码、调试程序,逐步实现词法分析器的各个功能模块。
调试时间:在程序编写过程中不断进行调试,解决出现的各种问题,如语法错误、逻辑错误等,确保程序能够正确运行。

(二)遇到的问题及解决方法
1.注释处理问题:在处理多行注释时,遇到了嵌套注释的情况,导致程序逻辑错误。后来通过增加状态标记来记录注释嵌套层次,正确处理了嵌套注释的情况。

2.字符串转换问题:在将识别出的数字字符串转换为整数值时,没有考虑到可能出现的非数字字符导致转换失败的情况。添加了错误处理机制,确保在遇到非法数字字符串时能够给出提示信息,而不影响程序的继续执行。

3.符号识别冲突:在识别运算符时,遇到单字符和双字符运算符(如 = 和 ==)的冲突问题。为了解决这一问题,我引入了优先处理双字符运算符的逻辑,即在判断当前字符是否为双字符运算符时,先判断是否符合双字符规则,若不符合再处理为单字符运算符。

4.边界问题:在文件的处理和词法单元的识别过程中,经常出现边界值问题,如字符串结尾和多余空白符的处理。通过在自动机状态转换中添加边界判断条件,确保了程序的稳定性。

(三)收获、体会和建议
1.收获与体会
深入理解了词法分析的原理和过程,通过实际编写词法分析器,对自动机的设计和运用有了更直观的认识。
提高了编程能力,学会了如何运用 C++ 语言和相关数据结构(如 map、vector、string 等)来实现复杂的功能,同时在代码调试过程中积累了丰富的经验。
对软件开发过程中的分析模型和设计模型有了更深入的理解,明白了如何通过合理的设计来提高程序的可读性、可维护性和可扩展性。

2.建议
在实验指导中可以提供更多关于自动机设计和实现的示例,帮助大家更好地理解和掌握相关知识。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Huazzi_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值