迷你编译器

迷你编译器详细文档

一、编译器是什么?

1.1 编译器的作用

想象一下,你写了一段C++代码,但计算机只能理解0和1。编译器就是那个"翻译官",把你写的高级语言代码翻译成计算机能懂的机器语言。

复制代码
你的代码 → 编译器 → 机器能执行的代码

1.2 学习编译器的好处

  • 深入理解编程语言:了解代码背后发生了什么
  • 提高调试能力:知道错误是怎么产生的
  • 培养系统思维:学会将大问题分解成小问题

二、这个迷你编译器能做什么?

我们的编译器可以处理这样的简单代码:

c 复制代码
main() {
    a = 10 + 5 * 2;        // 计算表达式
    if (a > 20) {          // 条件判断
        b = a - 20;
    } else {
        b = 20 - a;
    }
    while (b < 100) {      // 循环
        b = b + 10;
    }
    return b;              // 返回值
}

然后把它变成x86汇编代码,最终可以被计算机执行。

三、编译器的五个阶段(像工厂流水线)

我们的编译器像一条生产线,源代码经过5个车间变成最终产品:

复制代码
源代码 → [词法分析] → [语法分析] → [中间代码] → [代码优化] → [目标代码]

四、详细分解每个模块

4.1 词法分析器(Scanner)- "拆零件车间"

4.1.1 它在做什么?

把一整段代码拆成一个个有意义的"单词",就像把一句话拆成单个词语。

输入a = 10 + 20;
输出[标识符"a", 运算符"=", 数字"10", 运算符"+", 数字"20", 分隔符";"]

4.1.2 为什么要这样做?

计算机需要知道每个部分的含义,就像人读文章需要知道每个词的意思。

4.1.3 核心代码详解
cpp 复制代码
// 定义Token(单词)的结构
struct Token {
    int type;     // 类型编号(比如1代表main,10代表变量名)
    string value; // 具体的值(比如"a"、"10")
    int line;     // 在第几行(用于错误提示)
};

类型编号的含义

  • 1-12:关键字(main、int、if、while等)
  • 10:变量名
  • 20:数字
  • 21-36:运算符(+、-、*、/、>、<等)
  • 0:结束标记
4.1.4 关键方法详解

1. recognizeIdentifier() - 识别变量名和关键字

cpp 复制代码
Token recognizeIdentifier() {
    string value;
    char ch = getChar();  // 读取一个字符
    
    if (isalpha(ch)) {    // 如果是字母开头
        value += ch;
        // 继续读,直到遇到非字母数字
        while (pos < length && isalnum(sourceCode[pos])) {
            value += sourceCode[pos];
            pos++;
        }
    }
    
    // 检查是不是关键字
    if (keywords.find(value) != keywords.end()) {
        return Token(keywords[value], value, line); // 返回关键字
    } else {
        return Token(10, value, line); // 返回普通变量名
    }
}

工作原理

  • 从第一个字母开始读取
  • 一直读到不是字母或数字为止
  • 检查读到的单词是不是关键字
  • 返回相应的Token

2. recognizeNumber() - 识别数字

cpp 复制代码
Token recognizeNumber() {
    string value;
    bool hasDot = false; // 有没有小数点
    char ch = getChar();
    
    // 处理正负号
    if (ch == '+' || ch == '-') {
        value += ch;
        ch = getChar();
    }
    
    while (pos <= length) {
        if (isdigit(ch)) {
            value += ch;  // 数字直接加入
        } else if (ch == '.' && !hasDot) {
            value += ch;  // 第一个小数点加入
            hasDot = true;
        } else {
            break;  // 遇到其他字符就停止
        }
        ch = getChar();
    }
    
    return Token(20, value, line); // 返回数字Token
}

3. recognizeOperator() - 识别运算符

cpp 复制代码
Token recognizeOperator() {
    string value;
    char ch = getChar();
    value += ch;
    
    // 检查是不是双字符运算符(比如>=、<=)
    if (pos < length) {
        char nextCh = sourceCode[pos];
        string doubleOp = value + nextCh;
        if (operators.find(doubleOp) != operators.end()) {
            value = doubleOp;
            pos++;  // 多读一个字符
        }
    }
    
    return Token(operators[value], value, line);
}

为什么要检查双字符?

因为>>=是两个不同的运算符,需要区分。

4.2 语法分析器(Parser)- "检查语法车间"

4.2.1 它在做什么?

检查Token序列是否符合语法规则,并理解代码的结构。

就像检查"我 吃 饭"符合"主语+谓语+宾语"的语法规则。

4.2.2 为什么要这样做?

确保代码结构正确,为生成中间代码做准备。

4.2.3 核心语法规则

我们使用这些规则来分析表达式:

复制代码
E → E + T | E - T | T      (表达式由加减运算组成)
T → T * F | T / F | F      (项由乘除运算组成)  
F → id | num | (E)         (因子是变量、数字或括号表达式)

这是什么意思?

  • E(Expression):表达式
  • T(Term):项
  • F(Factor):因子
  • :可以推导为
  • |:或者

实际例子分析a + b * c

  1. 先看成 E + T
  2. Tb * c,即 T * F
  3. 最终结构:a + (b * c)
4.2.4 关键方法详解

1. E() - 表达式分析

cpp 复制代码
string E() {
    string left = T();  // 先分析左边的项
    
    // 如果后面有+或-,继续分析
    while (getCurrentToken().type == 22 || getCurrentToken().type == 23) {
        string op = getCurrentToken().value;  // 获取运算符
        match(getCurrentToken().type);        // 消耗掉这个运算符
        string right = T();                   // 分析右边的项
        left = ig->genArithmetic(left, op, right); // 生成中间代码
    }
    return left;
}

工作流程

  • 先调用T()分析第一个项
  • 检查后面有没有+-
  • 如果有,继续分析后面的项
  • 生成对应的中间代码

2. parseCondition() - 分析if语句

cpp 复制代码
void parseCondition() {
    match(6);  // 匹配if关键字
    match(26); // 匹配左括号(
    
    // 分析条件表达式
    string left = E();
    string op = getCurrentToken().value;
    match(getCurrentToken().type); // 匹配比较运算符
    string right = E();
    
    match(27); // 匹配右括号)
    
    // 生成标签(像书签一样,用于跳转)
    string falseLabel = ig->newLabel();  // else部分的入口
    string endLabel = ig->newLabel();    // if语句结束
    
    // 生成条件跳转:如果条件不成立,跳到else部分
    ig->genConditionJump(left, op, right, falseLabel);
    
    // 分析if成立时要执行的代码块
    match(28); // 匹配{
    parseStatementBlock();
    match(29); // 匹配}
    
    // 跳过else部分,直接到结束
    ig->genUnconditionalJump(endLabel);
    
    // 标记else部分开始
    ig->genLabel(falseLabel);
    
    // 如果有else,分析else部分
    if (getCurrentToken().type == 7) {
        match(7);  // 匹配else
        match(28); // 匹配{
        parseStatementBlock();
        match(29); // 匹配}
    }
    
    // 标记if语句结束
    ig->genLabel(endLabel);
}

if语句的翻译思路

复制代码
如果条件不成立 → 跳转到else标签
执行if代码块
跳转到结束标签(跳过else部分)
else标签:
执行else代码块  
结束标签:
继续后面的代码

4.3 中间代码生成器(IntermediateGenerator)- "生成通用指令车间"

4.3.1 它在做什么?

生成一种介于源代码和机器代码之间的通用指令格式------四元式。

4.3.2 为什么要用中间代码?
  • 便于优化:在中间代码层面做优化比较容易
  • 便于移植:换目标机器时,只需要改后半部分
  • 简化设计:把复杂问题分解
4.3.3 四元式是什么?

四元式格式:(操作数1, 运算符, 操作数2, 结果)

例子c = a + b * 2 会被翻译成:

复制代码
(2, *, b, T1)    // T1 = 2 * b
(a, +, T1, T2)   // T2 = a + T1  
(T2, =, , c)     // c = T2
4.3.4 核心代码详解

1. 四元式结构

cpp 复制代码
struct FourCom {
    string arg1;    // 第一个参数
    string opera;   // 操作符
    string arg2;    // 第二个参数  
    string result;  // 结果
};

2. 生成算术运算

cpp 复制代码
string genArithmetic(string arg1, string op, string arg2) {
    string temp = newTemp();  // 生成临时变量名,比如T1、T2
    addFourCom(arg1, op, arg2, temp);  // 添加四元式
    return temp;  // 返回临时变量名,供后面使用
}

3. 生成条件跳转

cpp 复制代码
void genConditionJump(string arg1, string op, string arg2, string label) {
    addFourCom(arg1, op, arg2, label);
}
// 例子:(a, >, b, L1) 意思是:如果a>b,跳转到标签L1

4. 管理临时变量

cpp 复制代码
string newTemp() {
    return "T" + to_string(tempCounter++);
}
// 第一次调用返回"T0",第二次返回"T1",以此类推

4.4 中间代码优化器(Optimizer)- "优化车间"

4.4.1 它在做什么?

对中间代码进行优化,让最终的程序运行更快、占用空间更小。

4.4.2 为什么要优化?

有些计算在编译时就能确定结果,不需要等到运行时。

4.4.3 常量折叠优化

优化前

复制代码
(5, *, 2, T1)    // T1 = 5 * 2
(10, +, T1, T2)  // T2 = 10 + T1

优化后

复制代码
(20, =, , T2)    // T2 = 20 (直接计算出结果)
4.4.4 核心代码详解
cpp 复制代码
vector<FourCom> constantFolding(const vector<FourCom>& fourComs) {
    vector<FourCom> optimized;
    
    for (const auto& com : fourComs) {
        // 检查是否是算术运算,并且两个操作数都是数字
        if ((com.opera == "+" || com.opera == "-" || 
             com.opera == "*" || com.opera == "/") &&
            isNumber(com.arg1) && isNumber(com.arg2)) {
            
            // 在编译时计算结果
            double result = calculate(com.arg1, com.opera, com.arg2);
            // 替换为简单的赋值
            optimized.push_back(FourCom(to_string(result), "=", "", com.result));
        } else {
            optimized.push_back(com);  // 无法优化,直接保留
        }
    }
    return optimized;
}

4.5 目标代码生成器(AssemblyGenerator)- "生成最终产品车间"

4.5.1 它在做什么?

把四元式翻译成x86汇编代码,这是计算机能直接理解的语言。

4.5.2 为什么要生成汇编?

汇编是机器指令的文本表示,再通过汇编器就能变成真正的机器代码。

4.5.3 核心代码详解

1. 处理赋值语句

cpp 复制代码
if (com.opera == "=") {
    if (isalpha(com.arg1[0])) {
        // 如果是变量:MOV EAX, [变量名]
        assembly.push_back("MOV EAX, [" + com.arg1 + "]");
    } else {
        // 如果是数字:MOV EAX, 数字
        assembly.push_back("MOV EAX, " + com.arg1);
    }
    assembly.push_back("MOV [" + com.result + "], EAX");
}

2. 处理加法

cpp 复制代码
else if (com.opera == "+") {
    assembly.push_back("MOV EAX, [" + com.arg1 + "]");  // 把第一个数放到EAX
    assembly.push_back("ADD EAX, [" + com.arg2 + "]");  // 加上第二个数
    assembly.push_back("MOV [" + com.result + "], EAX"); // 结果存回去
}

3. 处理条件跳转

cpp 复制代码
else if (com.opera == ">") {
    assembly.push_back("MOV EAX, [" + com.arg1 + "]");  // 加载第一个数
    assembly.push_back("CMP EAX, [" + com.arg2 + "]");  // 比较两个数
    assembly.push_back("JG " + com.result);             // 如果大于就跳转
}

五、完整编译流程示例

让我们跟踪一个简单例子的完整编译过程:

源代码:

c 复制代码
main() {
    a = 10 + 5 * 2;
}

5.1 词法分析结果:

复制代码
(1, "main")    (26, "(")    (27, ")")    (28, "{")
(10, "a")      (21, "=")    (20, "10")   (22, "+")
(20, "5")      (24, "*")    (20, "2")    (31, ";")
(29, "}")      (0, "#")

5.2 语法分析过程:

  1. 找到main函数
  2. 分析a = 10 + 5 * 2;
    • 先分析5 * 2(乘法优先级高)
    • 再分析10 + 结果

5.3 中间代码生成:

复制代码
(5, *, 2, T0)      // T0 = 5 * 2
(10, +, T0, T1)    // T1 = 10 + T0  
(T1, =, , a)       // a = T1

5.4 代码优化:

复制代码
(20, =, , a)       // 直接计算出10+5*2=20

5.5 目标代码生成:

asm 复制代码
.data
a dd ?
.code
main proc
    MOV EAX, 20     ; 把20放到EAX寄存器
    MOV [a], EAX    ; 把EAX的值存到变量a
main endp
end main

六、实现一个编译器

6.1 实现步骤

第一步:实现词法分析器

  1. 先识别单个字符
  2. 再识别关键字和变量名
  3. 最后识别数字和运算符
  4. 添加行号跟踪

第二步:实现简单的语法分析

  1. 先支持赋值语句:变量 = 表达式
  2. 再支持算术表达式
  3. 最后支持控制结构

第三步:生成中间代码

  1. 设计四元式结构
  2. 实现临时变量管理
  3. 生成基本的运算四元式

第四步:生成目标代码

  1. 选择一种目标语言(汇编、C等)
  2. 实现四元式到目标代码的翻译
  3. 测试简单程序

6.2 调试技巧

  • 分阶段测试:确保每个阶段都正确后再进入下一阶段
  • 打印中间结果:在关键步骤输出中间状态
  • 使用简单测试用例 :从a = 1;这样的简单代码开始

6.3 扩展想法

完成基础版本后,可以添加:

  • 更多的数据类型(浮点数、字符)
  • 函数调用
  • 数组支持
  • 更复杂的优化
  • 错误提示和恢复

七、总结

这个迷你编译器虽然简单,但包含了现代编译器的所有核心思想:

  1. 模块化设计:每个阶段职责明确
  2. 逐步翻译:从高级语言到低级语言的渐进转换
  3. 中间表示:使用四元式作为通用中间格式
  4. 优化思想:在编译时完成尽可能多的工作
相关推荐
止观止1 小时前
实战演练:用现代 C++ 重构一个“老项目”
c++·实战·raii·代码重构·现代c++
草莓熊Lotso4 小时前
unordered_map/unordered_set 使用指南:差异、性能与场景选择
java·开发语言·c++·人工智能·经验分享·python·网络协议
咔咔咔的6 小时前
1930. 长度为 3 的不同回文子序列
c++
行走的陀螺仪11 小时前
.vscode 文件夹配置详解
前端·ide·vscode·编辑器·开发实践
Cinema KI11 小时前
吃透C++继承:不止是代码复用,更是面向对象设计的底层思维
c++
Dream it possible!14 小时前
LeetCode 面试经典 150_二叉搜索树_二叉搜索树中第 K 小的元素(86_230_C++_中等)
c++·leetcode·面试
Bona Sun15 小时前
单片机手搓掌上游戏机(十四)—pico运行fc模拟器之电路连接
c语言·c++·单片机·游戏机
oioihoii15 小时前
性能提升11.4%!C++ Vector的reserve()方法让我大吃一惊
开发语言·c++
小狗爱吃黄桃罐头16 小时前
《C++ Primer Plus》模板类 Template 课本实验
c++