编译器是什么?
编译器是一种程序,它在程序运行之前,把用编程语言编写的源代码翻译成机器码,或者翻译成字节码等其他更底层的形式。翻译好的程序可以脱离原始源代码独立运行。
更新于 2026年9月24日
- 1print("Hello")
- 2total = 2 + 3
- 3print(total)
- 4print("Done")
编译一次之后,程序想运行多少次都可以:运行次数会增加,编译次数不变。打开拼写错误后,编译器会拒绝整个程序,所以什么都不会输出。
把下面这个 C 程序保存为 hello.c:
#include <stdio.h>
int main(void) {
int price = 4;
printf("Total: %d\n", price * 3);
return 0;
}
然后输入 gcc hello.c -o hello。片刻之间看不到任何变化,接着源文件旁边会出现一个名为 hello 的新文件。这个文件就是编译器的输出:处理器可以直接运行的机器码。即使删掉 hello.c,./hello 照样能运行,因为程序需要的一切都已经翻译好了。
gcc hello.c -o hello
./hello
Total: 12
没有安装 gcc?把 hello.c 粘贴到在线 C 编译器里就行。
编译器如何一步步工作
编译器把源代码当作纯文本读取,并分阶段处理。以一行代码 total = price * 3 为例:
- 词法分析。 文本被拆分成词法单元(token),也就是这门语言的“单词”:名字
total、运算符=、名字price、运算符*、数字3。空格和注释在这一步就被丢弃了。 - 语法分析。 词法单元被组织成一棵语法树,记录代码的结构:一个赋值语句,它的值是
price与3的乘积。违反语言语法的代码会在这里停下,报出语法错误。 - 语义分析。 编译器检查代码的含义:
price是否已经声明,类型是否兼容,被调用的函数是否存在。 - 优化。 在不改变程序行为的前提下改写程序,让它运行得更快或占用更少内存。
x * 2可能变成一条移位指令,永远不会执行的代码会被删除。 - 代码生成。 编译器为目标平台写出输出:x86-64 或 ARM 机器码,或者供虚拟机运行的字节码。
对于 C 和 C++,还有最后一步链接,它把你编译好的文件和库代码(比如 C 标准库中的 printf)合并成一个可执行文件。gcc 会替你运行链接器,所以感觉上只是一条命令。
Python 把前两个阶段作为标准模块提供,所以你可以用真实的输入看看它们是如何工作的:
NAME 'total'
OP '='
NAME 'price'
OP '*'
NUMBER '3'
Assign(
targets=[
Name(id='total', ctx=Store())],
value=BinOp(
left=Name(id='price', ctx=Load()),
op=Mult(),
right=Constant(value=3)))
编译器的后续阶段都基于这棵树来工作。它不再关心空格或换行,只关心结构:把 price 乘以 3 的结果赋给 total。
编译时:在程序运行前发现错误
由于编译器在任何代码运行之前就读完了整个程序,它可以提前拒绝错误。在下面的例子中,source 里的三行 print 一行都不会执行,连第一行也不会,因为 Python 的编译器在执行开始前就拒绝了这段文本:
Rejected before running anything:
'(' was never closed (line 3)
静态类型语言的编译器能发现的不只是语法错误。下面这行 C 代码位于一个名为 bad.c 的文件中,它在语法上完全正确,却把文本存进了一个整数变量:
int count = "five";
GCC 14 及更新版本会报错并停止:
bad.c:4:17: error: initialization of 'int' from 'char *' makes integer from pointer without a cast [-Wint-conversion]
同样的错误在 Python 中只有在那一行运行时才会暴露出来。能在编译时发现这类错误,是大型团队选择编译型、静态类型语言的原因之一。不过编译器无法发现所有问题:除以一个运行时才变成零的数,或者读取一个不存在的文件,仍然会在运行时失败。
编译器示例
| 编译器 | 读取 | 生成 |
|---|---|---|
| GCC、Clang | C、C++ | x86-64、ARM 等平台的机器码 |
| MSVC(Microsoft Visual C++) | C、C++ | Windows 可执行文件(.exe、.dll) |
javac | Java | JVM 字节码(.class 文件) |
rustc | Rust | 机器码 |
go build | Go | 单个可执行文件 |
tsc | TypeScript | JavaScript |
其中两行说明“机器码”并不是唯一可能的输出。javac 生成由 Java 虚拟机运行的字节码,而 Java 虚拟机是 Java 运行时环境的一部分。tsc 把 TypeScript 转换成另一种高级语言 JavaScript;做这种事的编译器通常被称为转译器(transpiler)。
编译出来的机器码绑定在一种 CPU 架构和一种操作系统上。为 x86-64 上的 Windows 编译的程序,无法在搭载 Apple M 系列芯片的 Mac 上运行;你需要为每个目标平台把同一份源代码重新编译一次。
编译器还是解释器
解释器直接运行源代码,而不生成单独的程序文件,Python 和 Ruby 通常就是这样运行的。大多数现代语言工具同时使用了这两种思路:CPython 先把你的文件编译成字节码再解释执行,JVM 和 JavaScript 的 V8 引擎则在程序运行期间把频繁使用的代码编译成机器码。完整的对比表格见编译器与解释器的区别页面。
常见误解
- “能编译通过,就能正常工作。” 编译器检查的是程序是否有效,而不是是否正确。逻辑错误和除以零这样的运行时故障都能通过编译。
- “Python 不需要编译。” CPython 在运行每个文件之前都会把它编译成字节码。
__pycache__文件夹里的.pyc文件就是这些字节码,它们是为程序导入的模块保存的,这样下次运行就可以跳过这一步。 - “在线编译器就是编译器。” 在浏览器里运行代码的网站会把代码发送到服务器,由服务器运行完整的工具链(编译器、链接器以及程序本身),再把输出返回给你。编译器只是其中的一环。
- “编译后的代码在哪里都能运行。” 它只能在构建时指定的 CPU 和操作系统上运行。可移植性来自重新编译,或者来自 JVM 这样的虚拟机。
下一步学什么
C 语言文档展示了编译器的日常用法:如何编译和运行 C 程序用真实的 gcc 命令,带你走完预处理、编译、汇编和链接的全过程。想了解运行代码的另一种方式,可以阅读编译器与解释器的区别,然后通过运行时环境了解程序启动后周围的一切。C 语言课程是用编译型语言动手练习的好地方。
常见问题
编译器由哪些部分组成?
编译器通常被分成三个部分来介绍。前端读取源代码,把它拆分成词法单元,解析成语法树并进行类型检查。中端对程序的中间表示进行优化,后端则为目标 CPU 生成机器码或字节码。
编译器怎么能编译它自己?
靠自举(bootstrapping)。编译器的第一个版本用一种已有的语言编写,或者由更早的编译器来编译,然后再用它去编译用新语言本身编写的下一个版本。比如 GCC 主要用 C 和 C++ 编写,由现有的 C++ 编译器(通常是更早版本的 GCC)构建而成。
编译器和汇编器有什么区别?
编译器翻译的是 C 或 Rust 这样的高级语言,其中一行代码可能变成许多条机器指令。汇编器翻译的是汇编语言,其中每一行几乎一一对应一条机器指令。GCC 等编译器通常会先生成汇编代码,再交给汇编器处理。
什么是 JIT 编译器?
即时(JIT)编译器在程序运行期间把代码翻译成机器码,而不是在运行之前。它会观察哪些部分执行得最频繁,然后编译这些部分,所以长时间运行的程序会越跑越快。JVM、JavaScript 的 V8 引擎和 PyPy 都使用 JIT 编译。
学编程需要先安装编译器吗?
一开始不需要。基于浏览器的编辑器和在线编辑器会在服务器上替你运行编译器或解释器。等你转到自己的电脑上时,C 和 C++ 需要 GCC 或 Clang 这样的编译器,Java 需要 JDK,Python 则需要 Python 解释器。