ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C++手搓编译器:从词法分析到代码生成的完整实现指南

2026/8/9 6:06:31 拓冰建站 浏览量
C++手搓编译器:从词法分析到代码生成的完整实现指南 1. 项目概述为什么用C手搓一个编译器如果你对编程的理解还停留在“调用库函数实现业务逻辑”的层面那么亲手用C构建一个编译器无疑是打通任督二脉的终极修炼。这不仅仅是实现一个工具更是对计算机如何“理解”并“执行”你写的代码进行一次从底层开始的、全景式的深度探索。最近看到不少朋友在搜“C八股文”、“C面试题”说实话背那些东西远不如亲手实现一个编译器来得深刻。当你自己处理了词法分析、语法树构建、语义检查和代码生成那些面试里关于内存管理、多态实现、编译优化的问题瞬间就有了血肉。这个项目我们将从零开始构建一个能处理简单算术表达式和变量赋值的“迷你编译器”。它最终可能不会生成x86汇编而是生成一种我们自定义的、易于理解的中间指令或伪汇编。选择C是因为它足够底层能让我们精细控制内存和数据结构比如手搓链表来实现语法树同时又足够高级避免陷入汇编的繁琐细节。整个过程你会清晰地看到你写的a b c * 2是如何一步步被拆解、分析并最终“翻译”成机器可执行的动作序列的。这比任何理论教材都来得直观。2. 编译器整体架构与核心模块设计一个编译器无论大小其核心工作流都可以抽象为一条清晰的“流水线”。我们的迷你编译器也将遵循经典的设计主要包含四个核心阶段。2.1 核心阶段分解从源代码到目标代码词法分析器这是编译器的“眼睛”。它的任务是把源代码字符串比如int a 5 3;切割成一个个有意义的“单词”在编译原理中称为“词法单元”或“Token”。例如它会识别出int是关键字a是标识符是操作符5是整型字面量是操作符3是整型字面量;是分号。这个过程会过滤掉空格、换行、注释等无关字符。语法分析器这是编译器的“大脑语法区”。它接收Token流并根据预定义的语法规则通常用上下文无关文法描述检查这些Token的排列组合是否符合语法。如果符合它会构建出一棵“抽象语法树”。这棵树以层次化的结构表达了代码的语法结构。例如表达式5 3 * 2对应的AST会明确表示乘法*的优先级高于加法而不是简单的从左到右。语义分析器可选但建议实现这是编译器的“逻辑校验器”。它在AST的基础上进行检查程序是否“有意义”。例如变量在使用前是否已声明赋值语句左右两边的类型是否匹配函数调用的参数个数和类型是否正确对于我们的迷你编译器可以实现简单的类型检查和变量作用域管理。代码生成器这是编译器的“翻译官”。它遍历经过语义检查的AST为每一种语法结构如赋值、算术运算、条件判断生成对应的目标代码。目标代码可以是真实的汇编如x86、ARM也可以是我们自定义的虚拟机字节码或简单的三地址码这取决于项目的复杂度和目标。2.2 数据结构设计Token与AST节点在编码之前我们需要定义两个贯穿始终的核心数据结构。Token结构体用于承载词法分析的结果。struct Token { enum class Type { Keyword, // 如 int, if, while Identifier, // 如变量名 a, count Literal, // 如数字 42, 字符串 hello Operator, // 如 , -, *, /, , Delimiter, // 如 ;, ,, (, ), {, } EndOfFile // 文件结束标记 }; Type type; std::string value; // Token对应的原始字符串 size_t line; // 所在行号用于错误提示 size_t column; // 所在列号 Token(Type t, const std::string v, size_t l, size_t c) : type(t), value(v), line(l), column(c) {} };AST节点基类与派生类用于构建语法树。这里采用多态来实现不同类型的节点。// AST节点基类 class ASTNode { public: virtual ~ASTNode() default; virtual void print(int indent 0) const 0; // 用于调试打印树结构 // 后续可以添加 accept(Visitor) 方法用于访问者模式便于代码生成和优化 }; // 二元运算节点例如 a b, c d class BinaryOpNode : public ASTNode { public: Token op; // 操作符 Token std::unique_ptrASTNode left; std::unique_ptrASTNode right; BinaryOpNode(Token opTok, std::unique_ptrASTNode l, std::unique_ptrASTNode r) : op(std::move(opTok)), left(std::move(l)), right(std::move(r)) {} void print(int indent) const override { // 打印逻辑展示树形结构 } }; // 字面量节点例如数字 5 class LiteralNode : public ASTNode { public: Token valueToken; LiteralNode(Token tok) : valueToken(std::move(tok)) {} void print(int indent) const override { /* ... */ } }; // 标识符节点例如变量名 a class IdentifierNode : public ASTNode { public: Token idToken; IdentifierNode(Token tok) : idToken(std::move(tok)) {} void print(int indent) const override { /* ... */ } };设计心得使用std::unique_ptrASTNode来管理子树所有权可以避免内存泄漏的麻烦也清晰地表达了树的层次关系。Token中保存行列信息在报错时极其有用能快速定位源代码中的问题位置。3. 词法分析器编译器的“分词引擎”词法分析器也叫扫描器是编译器的第一道工序。它的实现本质是一个确定有限状态自动机。我们不需要手动画状态转移图而是可以通过顺序扫描字符并配合条件判断来实现。3.1 核心扫描逻辑与状态判断我们创建一个Lexer类其核心是一个getNextToken()方法。class Lexer { private: std::string sourceCode; size_t position; // 当前读取位置 size_t line; size_t column; char currentChar() const { return position sourceCode.length() ? sourceCode[position] : \0; } void advance() { /* 移动 position更新 column 和 line */ } public: Lexer(const std::string code) : sourceCode(code), position(0), line(1), column(1) {} Token getNextToken(); };getNextToken()的内部实现就是一系列if-else或switch判断跳过空白遇到空格、制表符、换行符直接跳过并更新行列计数器。识别数字如果当前字符是数字则持续读取直到非数字字符形成一个完整的数字字面量字符串生成Token::Type::Literal。识别标识符和关键字如果当前字符是字母或下划线则持续读取字母、数字或下划线。读取完成后检查该字符串是否是预定义的关键字如int,if。如果是生成Token::Type::Keyword否则生成Token::Type::Identifier。识别操作符和分隔符对于单字符操作符如,-,*,/,;,,,(,)等直接生成对应Token。对于可能的多字符操作符如,!,,需要“向前看”一个字符peek来判断。处理注释如果遇到//则一直读到行尾并丢弃如果遇到/*则需要一直读到*/出现。这是一个容易出错的地方特别是嵌套注释我们通常不支持。文件结束当读到字符串末尾时返回一个Token::Type::EndOfFile。3.2 关键实现细节与避坑指南Token Lexer::getNextToken() { // 1. 跳过空白字符 while (std::isspace(currentChar())) { if (currentChar() \n) { line; column 1; } else { column; } advance(); } // 2. 处理文件结束 if (currentChar() \0) { return Token(Token::Type::EndOfFile, , line, column); } // 3. 识别数字字面量 if (std::isdigit(currentChar())) { size_t startCol column; std::string numStr; while (std::isdigit(currentChar())) { numStr currentChar(); advance(); column; } // 这里可以扩展支持浮点数、十六进制等 return Token(Token::Type::Literal, numStr, line, startCol); } // 4. 识别标识符和关键字 if (std::isalpha(currentChar()) || currentChar() _) { size_t startCol column; std::string idStr; while (std::isalnum(currentChar()) || currentChar() _) { idStr currentChar(); advance(); column; } // 关键字判断 static const std::unordered_setstd::string keywords {int, if, else, while, return}; if (keywords.find(idStr) ! keywords.end()) { return Token(Token::Type::Keyword, idStr, line, startCol); } return Token(Token::Type::Identifier, idStr, line, startCol); } // 5. 识别操作符和分隔符 char c currentChar(); size_t startCol column; advance(); column; switch (c) { case : return Token(Token::Type::Operator, , line, startCol); case -: return Token(Token::Type::Operator, -, line, startCol); case *: return Token(Token::Type::Operator, *, line, startCol); case /: // 处理注释 if (currentChar() /) { // 单行注释 while (currentChar() ! \0 currentChar() ! \n) { advance(); } column 1; line; advance(); // 吃掉换行符 return getNextToken(); // 递归调用获取注释后的下一个Token } else if (currentChar() *) { // 多行注释 // 实现略需要小心处理未闭合的注释 advance(); column; while (!(currentChar() * peekChar() /)) { if (currentChar() \0) { throw std::runtime_error(Unterminated comment at line std::to_string(line)); } if (currentChar() \n) { line; column 1; } else { column; } advance(); } advance(); advance(); // 跳过 */ column 2; return getNextToken(); } else { return Token(Token::Type::Operator, /, line, startCol); } case : if (currentChar() ) { advance(); column; return Token(Token::Type::Operator, , line, startCol); } return Token(Token::Type::Operator, , line, startCol); case ;: return Token(Token::Type::Delimiter, ;, line, startCol); case (: return Token(Token::Type::Delimiter, (, line, startCol); case ): return Token(Token::Type::Delimiter, ), line, startCol); case {: return Token(Token::Type::Delimiter, {, line, startCol); case }: return Token(Token::Type::Delimiter, }, line, startCol); // ... 其他字符处理 default: throw std::runtime_error(Unexpected character std::string(1, c) at line std::to_string(line) : std::to_string(startCol)); } }避坑指南行列计数在advance()函数中统一管理行列号更新比在每个判断分支里手动更新更可靠避免遗漏。“向前看”字符实现一个peekChar()函数查看下一个字符但不移动position对于识别,!,,等多字符操作符至关重要。错误恢复简单的词法分析器在遇到无法识别的字符时可以直接报错退出。更健壮的实现可以尝试跳过非法字符并继续扫描但这对初学者来说增加了复杂度。注释处理务必在注释处理逻辑的末尾递归调用getNextToken()或者使用循环以确保返回的是注释之后的有效Token而不是直接返回。这是新手常犯的错误。4. 语法分析器构建程序的“语法树”语法分析器也叫解析器是编译器的核心。它将线性的Token序列根据语法规则组装成树形的抽象语法树。我们采用递归下降分析法来实现这是最直观、最适合手写解析器的方法。它的核心思想是为语法规则中的每一个非终结符如“表达式”、“语句”编写一个对应的解析函数。4.1 文法定义与递归下降函数映射首先我们需要为我们的迷你语言定义一个简单的文法。这里使用扩展巴科斯范式EBNF描述program : statement* statement : declaration | assignment | ifStatement | whileStatement | block declaration : int identifier ( expression)? ; assignment : identifier expression ; ifStatement : if ( expression ) block (else block)? whileStatement : while ( expression ) block block : { statement* } expression : additiveExpression additiveExpression : multiplicativeExpression (( | -) multiplicativeExpression)* multiplicativeExpression : primaryExpression ((* | /) primaryExpression)* primaryExpression : identifier | literal | ( expression )根据这个文法我们可以在Parser类中创建对应的解析函数parseProgram()- 解析整个程序parseStatement()- 解析语句parseDeclaration()- 解析变量声明parseExpression()- 解析表达式parseAdditiveExpression()- 解析加减表达式parseMultiplicativeExpression()- 解析乘除表达式parsePrimaryExpression()- 解析基本表达式标识符、字面量、括号表达式4.2 表达式解析与运算符优先级处理表达式解析是语法分析中最经典的部分关键在于处理运算符的优先级乘除高于加减和结合性左结合。递归下降法通过函数调用的层次来天然地体现优先级。class Parser { private: Lexer lexer; Token currentToken; void eat(Token::Type expectedType) { if (currentToken.type expectedType) { currentToken lexer.getNextToken(); } else { throw std::runtime_error(Syntax error: expected ... at line std::to_string(currentToken.line)); } } Token peekToken() { /* 偷看下一个Token不消费 */ } public: Parser(Lexer l) : lexer(l) { currentToken lexer.getNextToken(); // 初始化获取第一个Token } // 解析表达式入口 std::unique_ptrASTNode parseExpression() { return parseAdditiveExpression(); } // 解析加减表达式 std::unique_ptrASTNode parseAdditiveExpression() { // 先解析一个乘除表达式作为左子树 auto left parseMultiplicativeExpression(); // 循环处理连续的加减操作符 while (currentToken.type Token::Type::Operator (currentToken.value || currentToken.value -)) { Token op currentToken; eat(Token::Type::Operator); // 消费掉操作符Token auto right parseMultiplicativeExpression(); // 解析右子树同样是乘除表达式 // 将当前的左子树和新的右子树用刚读到的操作符组合成一个新的二元运算节点 left std::make_uniqueBinaryOpNode(std::move(op), std::move(left), std::move(right)); } return left; // 返回构建好的子树 } // 解析乘除表达式 std::unique_ptrASTNode parseMultiplicativeExpression() { auto left parsePrimaryExpression(); // 先解析基本表达式 while (currentToken.type Token::Type::Operator (currentToken.value * || currentToken.value /)) { Token op currentToken; eat(Token::Type::Operator); auto right parsePrimaryExpression(); left std::make_uniqueBinaryOpNode(std::move(op), std::move(left), std::move(right)); } return left; } // 解析基本表达式标识符、字面量或括号表达式 std::unique_ptrASTNode parsePrimaryExpression() { if (currentToken.type Token::Type::Identifier) { auto node std::make_uniqueIdentifierNode(currentToken); eat(Token::Type::Identifier); return node; } else if (currentToken.type Token::Type::Literal) { auto node std::make_uniqueLiteralNode(currentToken); eat(Token::Type::Literal); return node; } else if (currentToken.type Token::Type::Delimiter currentToken.value () { eat(Token::Type::Delimiter); // 吃掉 ( auto expr parseExpression(); // 递归解析括号内的表达式 if (!(currentToken.type Token::Type::Delimiter currentToken.value ))) { throw std::runtime_error(Expected )); } eat(Token::Type::Delimiter); // 吃掉 ) return expr; } else { throw std::runtime_error(Unexpected token in primary expression); } } // 解析赋值语句 std::unique_ptrASTNode parseAssignment() { // 假设当前Token是标识符 Token idToken currentToken; eat(Token::Type::Identifier); Token opToken currentToken; // 应该是 eat(Token::Type::Operator); auto expr parseExpression(); eat(Token::Type::Delimiter); // 吃掉 ; return std::make_uniqueBinaryOpNode(std::move(opToken), std::make_uniqueIdentifierNode(std::move(idToken)), std::move(expr)); } };实操心得eat()函数是核心它负责消费匹配一个期望类型的Token如果类型不匹配则报错。这是递归下降解析器的“推进器”。优先级是通过函数调用层次实现的parseExpression-parseAdditiveExpression-parseMultiplicativeExpression-parsePrimaryExpression。调用链越深优先级越高。parseAdditiveExpression在遇到乘除号时会调用优先级更高的parseMultiplicativeExpression来处理。左结合性的实现注意在parseAdditiveExpression和parseMultiplicativeExpression中的while循环。对于表达式a b c它会被解析为((a b) c)这正是左结合。循环不断将新的操作符和右子树与当前的左子树结合构建出正确的AST。错误处理目前的错误处理很简陋。一个更健壮的解析器应该能尝试从错误中恢复比如跳过直到下一个分号并收集多个错误一次性报告。5. 语义分析为程序注入“逻辑灵魂”语法分析只关心“形式”是否正确而语义分析则关心“含义”是否合法。对于我们的迷你编译器语义分析主要做两件事符号表管理和类型检查。5.1 符号表的设计与作用域管理符号表是一个数据结构用于记录程序中所有声明过的标识符主要是变量的信息包括名称、类型、作用域、内存位置如果后续需要分配等。最简单的实现是一个std::unordered_mapstd::string, Symbol。作用域是语义分析的关键。当进入一个代码块由{}包围时就进入了一个新的作用域。新的声明会添加到当前作用域。当离开这个代码块时该作用域内的所有声明都应该失效。这通常通过“作用域栈”来实现。struct Symbol { std::string name; std::string type; // 例如 int // 其他属性如是否初始化内存偏移量等 }; class SymbolTable { private: std::vectorstd::unordered_mapstd::string, Symbol scopes; // 作用域栈 public: void enterScope() { scopes.push_back({}); } void exitScope() { if (!scopes.empty()) { scopes.pop_back(); } } bool addSymbol(const Symbol sym) { if (scopes.empty()) enterScope(); auto currentScope scopes.back(); // 检查当前作用域是否已存在同名符号禁止重复声明 if (currentScope.find(sym.name) ! currentScope.end()) { return false; // 重复声明错误 } currentScope[sym.name] sym; return true; } std::optionalSymbol lookup(const std::string name) { // 从最内层作用域向外查找 for (auto it scopes.rbegin(); it ! scopes.rend(); it) { auto found it-find(name); if (found ! it-end()) { return found-second; } } return std::nullopt; // 未找到 } };5.2 类型检查与AST遍历语义分析需要对AST进行遍历。我们可以采用访问者模式为每种AST节点定义相应的“访问”行为。这里为了简化我们直接在解析过程中进行简单的语义检查。例如在parseDeclaration函数中当遇到int a;时调用symbolTable.addSymbol({a, int})。在parseAssignment或parseExpression中当遇到一个标识符时在parsePrimaryExpression里调用symbolTable.lookup(identifierName)。如果返回std::nullopt则报错“未声明的变量”。在parseAssignment中赋值号左边的表达式必须是一个“左值”目前就是标识符。同时可以检查赋值号左右两边的类型是否兼容在我们的迷你语言里暂时只有int类型所以总是兼容。更复杂的类型检查比如检查if条件表达式的结果是否为布尔类型或者函数调用参数匹配都需要在对应的解析函数中嵌入检查逻辑。注意事项语义分析阶段是发现诸如“使用了未定义的变量”、“类型不匹配”等逻辑错误的最佳时机。将符号表管理好能极大简化后续代码生成阶段的工作尤其是为变量分配存储位置时。6. 代码生成从AST到可执行指令代码生成是编译器的最后一步也是最贴近机器的一步。为了简化我们不直接生成x86汇编而是生成一种简单的三地址码或栈式虚拟机字节码。这里以生成一种人类可读的伪指令为例。6.1 目标代码选择三地址码简介三地址码的基本形式是result arg1 op arg2。每个指令最多涉及三个地址变量或临时变量。它非常接近现代处理器的指令但又独立于具体硬件。例如对于表达式x a b * c可能生成以下三地址码序列t1 b * c t2 a t1 x t26.2 遍历AST生成指令我们同样通过遍历AST来生成代码。为每种AST节点类型编写一个代码生成方法。class CodeGenerator { private: std::vectorstd::string instructions; int tempVarCounter 0; SymbolTable symTable; // 需要符号表来查询变量信息 std::string newTemp() { return t std::to_string(tempVarCounter); } public: CodeGenerator(SymbolTable st) : symTable(st) {} std::string generateCode(ASTNode* root) { // 假设root是一个语句列表的节点 // 遍历所有语句为每个语句生成代码 // 这里简化处理假设root是单个表达式或赋值语句 return evaluate(root); // evaluate函数返回一个“地址”变量名或临时变量名 } // 评估一个表达式节点返回存储结果的变量名 std::string evaluate(ASTNode* node) { if (auto* binOp dynamic_castBinaryOpNode*(node)) { std::string leftAddr evaluate(binOp-left.get()); std::string rightAddr evaluate(binOp-right.get()); if (binOp-op.value ) { // 赋值语句 // 假设左子树是IdentifierNode auto* leftId dynamic_castIdentifierNode*(binOp-left.get()); if (!leftId) { /* 错误处理 */ } instructions.push_back(leftId-idToken.value rightAddr); return leftId-idToken.value; } else { // 算术运算 std::string temp newTemp(); instructions.push_back(temp leftAddr binOp-op.value rightAddr); return temp; } } else if (auto* idNode dynamic_castIdentifierNode*(node)) { // 返回变量名本身 // 这里可以检查变量是否在符号表中已声明 return idNode-idToken.value; } else if (auto* litNode dynamic_castLiteralNode*(node)) { // 返回字面量值 return litNode-valueToken.value; } return ; } void printInstructions() const { for (const auto instr : instructions) { std::cout instr std::endl; } } };对于更复杂的控制流如if,while代码生成需要处理标签和跳转指令。例如if (cond) { stmt1; } else { stmt2; }可以生成如下伪代码cond_code // 计算条件表达式结果存入某个临时变量或标志位 JUMP_IF_FALSE else_label cond_result stmt1_code JUMP end_label else_label: stmt2_code end_label:6.3 简单的优化考虑即使在迷你编译器中也可以实现一两个简单的优化让生成的代码更高效常量折叠在语法分析或语义分析阶段如果发现一个表达式的所有操作数都是常量如3 5 * 2可以直接计算出结果13并用一个LiteralNode代替整个子树避免生成无用的计算指令。公共子表达式消除如果同一表达式在同一个作用域内被计算多次可以将其结果存入一个临时变量后续直接使用该变量。这需要更复杂的数据流分析对于入门项目可以暂缓。代码生成心得代码生成器是“知道最多”的模块。它既要知道AST的结构又要知道目标指令集的特点还要管理临时变量和标签。设计一个清晰、模块化的代码生成器接口非常重要。可以考虑使用访问者模式将AST遍历和代码生成逻辑分离这样以后更换目标平台比如从伪指令换成真实汇编会更容易。7. 集成测试与常见问题排查将词法分析、语法分析、语义分析和代码生成模块串联起来就构成了一个完整的编译器前端。我们可以编写一个小型测试框架来验证各个模块。7.1 构建端到端测试流程创建一个简单的main函数int main() { std::string sourceCode R( int a; int b; a 5; b a 3 * 2; ); try { // 1. 词法分析 Lexer lexer(sourceCode); // 可以测试打印所有Token // Token tok; // do { // tok lexer.getNextToken(); // std::cout Token: tok.value std::endl; // } while (tok.type ! Token::Type::EndOfFile); // 2. 语法分析 语义分析 Parser parser(lexer); SymbolTable symTable; // 我们需要将symTable传递给Parser或者在Parser中创建并填充它 // 假设Parser有一个parseProgram方法返回AST根节点 std::unique_ptrASTNode astRoot parser.parseProgram(); // 3. 打印AST调试用 // astRoot-print(); // 4. 代码生成 CodeGenerator codeGen(symTable); codeGen.generateCode(astRoot.get()); codeGen.printInstructions(); } catch (const std::exception e) { std::cerr Compilation Error: e.what() std::endl; return 1; } return 0; }7.2 常见编译错误与调试技巧在开发编译器过程中你会遇到各种错误。以下是一个快速排查指南问题现象可能原因排查步骤词法分析器将关键字识别为标识符关键字集合未定义或匹配逻辑有误检查Lexer中的keywords集合确保在识别标识符后有关键字检查步骤。解析表达式12*3结果错误如计算为9运算符优先级处理错误检查parseAdditiveExpression和parseMultiplicativeExpression的调用关系。乘除表达式是否作为加减表达式的子过程被调用报告“未声明的变量”但变量已声明1. 作用域管理错误。2. 声明语句的解析未向符号表添加符号。3. 变量查找时作用域遍历顺序错误。1. 在进入/退出代码块时打印符号表状态。2. 单步调试addSymbol和lookup函数。3. 检查lookup是否是从内层作用域向外查找。生成的三地址码序列不符合预期1. AST构建错误。2. 代码生成器遍历AST的顺序错误。3. 临时变量生成逻辑混乱。1. 首先打印AST确认树的结构正确例如*节点是否在节点的下层。2. 在evaluate函数中添加日志打印当前处理的节点和生成的指令。3. 检查newTemp()的计数器是否在正确的作用域内重置通常不需要重置一直递增即可保证唯一性。遇到多字符操作符如解析失败词法分析器“向前看”(peek)逻辑有误或未实现。在Lexer中实现peekChar()函数。在识别时查看下一个字符是否是如果是则消费两个字符生成Token。注释后第一个Token丢失注释处理逻辑在吃掉注释字符后没有继续获取下一个有效Token。确保在//或/*...*/的处理分支末尾递归调用getNextToken()或使用循环回到函数开头。调试利器打印Token流在词法分析后将识别出的所有Token类型和值打印出来这是验证“分词”是否正确的最直接方法。打印AST为ASTNode实现一个格式化的print(int indent)方法可以直观地看到语法树的结构对于调试优先级和结合性错误至关重要。单元测试为每个模块Lexer,Parser中的各个函数编写小的单元测试例如测试parsePrimaryExpression是否能正确解析数字、变量和括号表达式。这能极大提升开发效率。8. 项目扩展与进阶思考完成一个基础编译器后你可以选择多个方向进行深化这会让你的理解再上一个台阶。8.1 支持更复杂的语言特性控制流实现if-else和while语句。这需要代码生成器支持标签和条件/无条件跳转指令。函数这是质的飞跃。需要处理函数声明、参数传递、调用约定、返回值和栈帧管理。你会深入理解“调用栈”的概念。数组和结构体引入更复杂的数据类型涉及到连续内存布局和地址计算。简单的类型系统加入float、bool、char类型并实现基本的类型转换规则。8.2 优化策略初探常量传播如果一个变量被赋值为常量那么在所有使用该变量的地方只要其值没有被重新赋值就可以直接用常量替换。死代码消除移除永远不会被执行到的代码如if (false) { ... }或者计算结果永远不会被使用的代码。窥孔优化在一个很小的指令窗口如相邻的两三条指令内寻找可以替换为更高效指令的模式。例如将t a 0优化为t a。8.3 转向真实目标平台生成LLVM IRLLVM提供了一个与硬件无关的中间表示层。将你的AST转换为LLVM IR然后利用LLVM强大的后端可以轻松生成x86、ARM等多种架构的优化后汇编代码。这是现代编译器如Clang的做法。生成x86汇编你可以学习x86汇编的基础知识然后自己将三地址码映射到有限的几条x86指令如mov,add,imul,cmp,jmp等并使用系统调用或调用C库函数来实现输入输出。这是最硬核、收获最大的方式能让你对程序如何在CPU上运行有刻骨铭心的理解。手写编译器是一个庞大的工程但这个迷你版本已经涵盖了所有核心概念。当你看到自己写的程序经过自己打造的编译器变成一串串指令并最终执行时那种成就感是无与伦比的。这不仅仅是学会了一项技能更是获得了一种深刻理解计算机系统的“元能力”。