Skip to content

编译原理入门

前言

当你按下"运行"按钮,代码是怎么变成屏幕上的结果的? 你写的每一行代码,计算机其实都"看不懂"——它只认识 0 和 1。编译器就是那个把人类语言翻译成机器语言的"翻译官"。理解编译原理,你就能理解报错信息从哪来、为什么有些语言快有些慢、以及代码优化的底层逻辑。

这篇文章会带你学什么?

学完这章后,你将获得:

  • 全局视野:掌握从源代码到可执行程序的完整编译流水线
  • 词法分析:理解编译器如何把代码拆成一个个 Token
  • 语法分析:理解 AST(抽象语法树)的构建过程
  • AST 可视化:直观看到代码的树形结构
  • 语义分析与优化:理解类型检查和代码优化的原理
  • 优化技术实战:掌握常量折叠、死代码消除等核心优化手段
  • 执行模型:区分编译型、解释型和 JIT 三种执行方式
章节内容核心概念
第 1 章编译器是什么翻译官类比、编译流水线
第 2 章词法分析Token、词法规则
第 3 章语法分析AST、语法树、优先级
第 4 章AST 可视化交互式语法树、节点类型
第 5 章语义分析与优化类型检查、常量折叠、死代码消除
第 6 章优化技术实战函数内联、循环外提、常量传播
第 7 章编译型 vs 解释型 vs JIT三种执行模型对比

0. 全景图:代码的"翻译之旅"

想象你是一个翻译官,要把一本中文小说翻译成英文。你不会一个字一个字地直译,而是:

  1. 识别词语 — 把句子拆成一个个词(词法分析)
  2. 理解句法 — 判断句子结构是否正确(语法分析)
  3. 理解语义 — 确保意思通顺、没有矛盾(语义分析)
  4. 润色优化 — 让译文更地道流畅(代码优化)
  5. 输出译文 — 写出最终的英文版本(代码生成)

编译器做的事情完全一样,只不过它翻译的是编程语言。

编译原理:翻译的艺术如何把代码翻译成机器指令
编译器就像翻译官,把人类能懂的代码翻译成机器能懂的指令
代码翻译的完整流程
1
词法分析
将代码分解成一个个单词(token)
int age = 25 → [int, age, =, 25]
2
语法分析
检查代码是否符合语法规则,构建语法树
验证语句结构是否正确
3
语义分析
检查代码的含义是否合理
检查变量是否定义、类型是否匹配
4
中间代码生成
生成与机器无关的中间表示
生成字节码或中间表示
5
优化
改进代码,提高执行效率
常量折叠、死代码消除
6
目标代码生成
生成机器码或目标代码
生成 x86、ARM 等机器指令
词法分析:分词
int age = 25;
关键字int
标识符age
运算符=
数字25
分隔符;
语法分析:构建树
赋值语句
变量age
运算符=
数字25
编译 vs 解释
编译型语言
源代码 → 编译器 → 机器码
C, Go, Rust
✓ 执行快
✓ 一次编译多次运行
✗ 编译慢
解释型语言
源代码 → 解释器 → 逐行执行
Python, JavaScript, PHP
✓ 开发快
✓ 跨平台
✗ 执行慢
编译器优化
优化前:
x = 5 + 3 + 2
⬇️
优化后:
x = 10
编译器会自动优化代码,提高运行效率

1. 编译器的六步流水线

编译器的工作可以分为六个阶段,像工厂流水线一样,每个阶段处理完交给下一个阶段。

编译器的工作流程从源代码到机器码的六步旅程
1
词法分析→ Token 流
2
语法分析→ AST 语法树
3
语义分析→ 带类型的 AST
4
中间代码生成→ IR(中间表示)
5
代码优化→ 优化后的 IR
6
目标代码生成→ 机器码
1词法分析输出:Token 流
把源代码拆成一个个"单词"(Token),就像读句子时先认出每个词
识别关键字识别标识符识别数字识别运算符过滤空白
int x = 10 + 5;
→ [int] [x] [=] [10] [+] [5] [;]
    关键字 标识符 运算符 数字 运算符 数字 分隔符
实时词法分析
intkeyword
xidentifier
=operator
10number
+operator
5number
;punctuation
三种执行方式对比
编译型
源码 编译器 机器码 CPU 执行
执行速度快需要编译等待
C, C++, Rust, Go
解释型
源码 解释器 逐行执行
即写即运行执行速度慢
Python, Ruby, PHP
JIT 即时编译
源码 字节码 JIT 热点编译 执行
兼顾性能和灵活启动较慢
Java, JavaScript (V8)
核心思想:编译器像翻译官,把人类能读懂的代码逐步翻译成机器能执行的指令。六个阶段各司其职:识别单词 → 理解语法 → 检查语义 → 生成中间码 → 优化 → 生成机器码。

编译流水线

  1. 词法分析(Lexical Analysis):把源代码拆成一个个 Token(单词)
  2. 语法分析(Syntax Analysis):把 Token 组织成语法树(AST)
  3. 语义分析(Semantic Analysis):检查类型是否正确、变量是否声明
  4. 中间代码生成(IR Generation):生成与平台无关的中间表示
  5. 代码优化(Optimization):让中间代码更高效
  6. 代码生成(Code Generation):生成目标平台的机器码
阶段输入输出类比
词法分析源代码字符流Token 流把句子拆成单词
语法分析Token 流AST(语法树)分析句子结构
语义分析AST带类型的 AST检查意思是否通顺
中间代码带类型的 ASTIR写出初稿
代码优化IR优化后的 IR润色删减
代码生成优化后的 IR机器码输出终稿

2. 词法分析:把代码拆成"单词"

词法分析是编译的第一步。编译器从左到右扫描源代码的每个字符,把它们组合成有意义的Token(词法单元)

🔤 词法分析器:把代码拆成 Token

输入一行代码,实时看到词法分析的结果

就像读英文句子时,你的大脑会自动把字母组合成单词一样,词法分析器把字符组合成 Token:

源代码: let x = 10 + 5;

Token 流:
[let]   → 关键字(语言保留字)
[x]     → 标识符(变量名)
[=]     → 运算符(赋值)
[10]    → 数字字面量
[+]     → 运算符(加法)
[5]     → 数字字面量
[;]     → 分隔符(语句结束)

Token 的五大类型

  • 关键字:语言保留的特殊单词,如 letifreturnfunction
  • 标识符:程序员定义的名字,如变量名、函数名
  • 字面量:直接写在代码里的值,如数字 42、字符串 "hello"
  • 运算符:执行运算的符号,如 +-====
  • 分隔符:分隔代码结构的符号,如 ;,()

3. 语法分析:构建语法树(AST)

词法分析把代码拆成了 Token,但 Token 只是一个个孤立的"单词"。语法分析的任务是把这些 Token 按照语法规则组织成一棵抽象语法树(Abstract Syntax Tree, AST)——它反映了代码的结构和运算优先级。

表达式: 1 + 2 * 3

语法树:        为什么这样?
       +       因为 * 的优先级
      / \      高于 +,所以
     1   *     2 * 3 先结合
        / \    成为一个子树
       2   3

AST 的重要性

AST 是编译器的"核心数据结构",后续的语义分析、优化、代码生成都基于它进行。现代开发工具也大量使用 AST:

  • ESLint:解析代码为 AST,检查是否违反规则
  • Prettier:解析为 AST 后重新格式化输出
  • Babel:解析 AST → 转换 → 生成兼容代码
  • IDE 重构:基于 AST 进行安全的变量重命名、函数提取
语法结构Token 序列AST 节点
变量声明let x = 10VariableDeclaration → Identifier + Literal
函数调用add ( 1 , 2 )CallExpression → Identifier + Arguments
条件语句if ( a > b )IfStatement → BinaryExpression + Block

4. AST 可视化:看见代码的"骨架"

上面我们用文字描述了 AST 的结构,但"看到"比"读到"更直观。下面的交互组件让你选择不同的表达式,实时观察它们的语法树长什么样。

🌳 AST 可视化:看见代码的"骨架"

选择一个表达式,观察它的抽象语法树结构

语法树
BinaryExpression+
NumericLiteral1
BinaryExpression*
NumericLiteral2
NumericLiteral3
解析说明
1* 优先级高于 +,所以 2 * 3 先结合
22 * 3 形成一个 BinaryExpression 子树
31 和这个子树作为 + 的左右操作数
4最终 + 是根节点,体现了运算顺序
💡 试试 AST Explorer — 在线查看任意代码的 AST

通过可视化你会发现,AST 的核心规律其实很简单:

代码结构AST 根节点子节点
1 + 2 * 3BinaryExpression (+)左: NumericLiteral(1),右: BinaryExpression(*)
let x = 10VariableDeclarationVariableDeclarator → Identifier(x) + NumericLiteral(10)
add(a, b)CallExpressionIdentifier(add) + Arguments(a, b)

AST 在日常开发中的应用

你可能没直接写过编译器,但你每天都在用基于 AST 的工具:

  • ESLint / Prettier:解析代码为 AST,检查规则或重新格式化
  • Babel / SWC:解析 AST → 转换语法 → 生成兼容代码
  • IDE 重构:基于 AST 做安全的重命名、提取函数
  • Tree-shaking:分析 AST 中的 import/export,删除未使用的代码

5. 语义分析与代码优化

语法分析确保代码"结构正确",但结构正确不代表"意思正确"。语义分析负责检查代码的含义是否合法,代码优化则让程序跑得更快。

编译过程实践从代码到可执行文件
输入代码
编译步骤
1
预处理
gcc -E hello.c -o hello.i
处理 #include,展开宏定义
2
编译
gcc -S hello.i -o hello.s
生成汇编代码
3
汇编
gcc -c hello.s -o hello.o
生成目标文件
4
链接
gcc hello.o -o hello
生成可执行文件
生成的文件
📄
hello.c
源代码文件
📝
hello.i
预处理后的文件
⚙️
hello.s
汇编代码文件
📦
hello.o
目标文件
🚀
hello
可执行文件
常用编译工具
GCC
GNU Compiler Collection
Clang
LLVM 的 C/C++ 编译器
MSVC
Microsoft Visual C++

4.1 语义分析:检查"意思"对不对

检查内容示例结果
类型检查int x = "hello"❌ 类型不匹配
作用域检查使用未声明的变量 y❌ 变量不存在
类型推断1 + 2.0✅ 推断结果为 float
参数检查add(1, 2, 3) 但函数只接受 2 个参数❌ 参数数量不匹配

你见过的报错,大多来自语义分析

  • TypeError: Cannot read properties of undefined — 类型检查
  • ReferenceError: x is not defined — 作用域检查
  • Expected 2 arguments, but got 3 — 参数检查

4.2 代码优化:让程序更快

编译器在生成最终代码前,会对中间代码做各种优化。这些优化对程序员透明,但能显著提升性能。

优化技术优化前优化后原理
常量折叠x = 10 + 5x = 15编译时直接算出结果
死代码消除if (false) { ... }直接删除永远不会执行的代码
常量传播x = 15; y = x * 2y = 30已知值直接替换
循环不变量外提循环内重复计算 len = arr.length提到循环外避免重复计算

6. 优化技术实战:编译器如何让代码更快

上面我们提到了几种优化技术的名字,现在来深入看看编译器具体是怎么做的。下面的交互组件展示了 5 种最常见的编译器优化,你可以直观对比优化前后的代码差异。

⚡ 编译器优化:让代码自动变快

选择一种优化技术,观察编译器如何自动改进你的代码

📝 优化前
const width = 10
const height = 20
const area = width * height  // 运行时计算
console.log(area)
编译器优化
🚀 优化后
const area = 200  // 编译时直接算出结果
console.log(200)
常量折叠原理
编译器发现 width 和 height 都是常量,在编译阶段就直接计算出 10 * 20 = 200,运行时不再需要做乘法运算。这是最基础也最常见的优化。
性能提升:
30%

现代编译器和 JIT 引擎(如 V8、GCC、LLVM)会自动应用数十种优化。作为开发者,你不需要手动做这些优化,但理解它们能帮你:

  • 写出更容易被优化的代码:比如用 const 而不是 let,编译器更容易做常量折叠
  • 理解性能差异:为什么小函数比大函数快?因为编译器能内联它们
  • 避免"反优化":某些写法会阻止编译器优化,比如 eval()with
优化技术触发条件性能影响开发者能做什么
常量折叠表达式中全是常量消除运行时计算多用 const 声明
死代码消除代码不可达或结果未使用减小代码体积及时清理无用代码
循环不变量外提循环内有不变的计算减少重复计算手动提取也是好习惯
函数内联小函数被频繁调用消除调用开销保持函数小而专注
常量传播变量值在编译时可确定整条计算链被消除用常量代替魔法数字

7. 编译型 vs 解释型 vs JIT

代码写完后,有三种"翻译方式"让它运行起来。这三种方式各有优劣,直接决定了语言的性能特征和使用场景。

🔄 编译型 vs 解释型 vs JIT

点击不同执行模式,观察代码从源码到运行的过程

📝
源代码
main.c
⚙️
编译器
全量编译
📦
机器码
二进制可执行文件
🚀
直接执行
CPU 直接运行
运行速度
极快
启动速度
慢(需编译)
跨平台
需重新编译
代表语言:CC++RustGo
维度编译型解释型JIT 即时编译
过程先全量编译成机器码,再执行边读边执行,逐行翻译先解释执行,热点代码再编译
运行速度最快最慢中等(热点接近编译型)
启动速度慢(需要编译)快(直接运行)中等(需要预热)
跨平台需要重新编译天然跨平台跨平台
代表语言C, Rust, GoPython, RubyJavaScript (V8), Java

为什么 JavaScript 这么快?

V8 引擎的 JIT 编译器会监测哪些代码被频繁执行(热点代码),然后把它们编译成高度优化的机器码。所以虽然 JavaScript 是"解释型语言",但在 V8 中它的性能可以接近编译型语言。这也是 Node.js 能做服务端的底气。


总结

编译原理不是只有编译器开发者才需要了解的知识。理解编译流程,能帮你更好地理解报错信息、选择合适的语言、写出更高效的代码。

回顾本章的关键要点:

  1. 编译器是翻译官:把人类可读的代码翻译成机器可执行的指令
  2. 六步流水线:词法分析 → 语法分析 → 语义分析 → 中间代码 → 优化 → 代码生成
  3. 词法分析拆 Token:把字符流拆成关键字、标识符、运算符等有意义的单元
  4. 语法分析建 AST:按语法规则把 Token 组织成树形结构,反映运算优先级
  5. 语义分析保正确:类型检查、作用域检查,你见过的大多数报错都来自这里
  6. 编译器自动优化:常量折叠、死代码消除、函数内联等技术让代码自动变快
  7. 三种执行模型:编译型最快、解释型最灵活、JIT 兼顾两者

延伸阅读