一句话说清

编译器把整份代码先翻译一遍,解释器边读边执行。

为什么值得懂

同一行加法,在 Python 里按下回车就出结果,在 C 里得先过一道编译;Java 编译出来的不是机器码,跑起来还要虚拟机再处理一次。搞清「谁在什么时候把代码变成动作」,一串日常问题就有了答案:为什么有的错误在运行前就报出来、有的要跑很久才炸;为什么换个语言同样的算法快慢差出几十倍。《编程思维》讲怎么把问题拆成机器能执行的步骤,这篇接着讲那些步骤是怎么被机器读懂的。

零基础讲解

一、两者共用同一套前端。 两者拿到源码后头几件事都一样:扫描字符、切出一个个有意义的词(词法分析),再按语法把词拼成树(语法分析),得到抽象语法树;然后检查名字有没有定义、类型对不对(语义分析)。阿霍等在《编译原理》第 1 章 1.2 节把这条流水线画成一张图,编译器与解释器并排站在同一张图里——它们的差别在最后一段,不在前面。

二、编译器的后端是「生成代码」。 走完前端,编译器把树翻成中间表示,做优化(能提前算的提前算,用不到的分支删掉),最后生成目标机器的指令交给操作系统执行。这条路一次做完,运行时不再需要编译器;代价是换一个平台要重新编译一遍。

三、解释器的后端是「当场求值」。 解释器不落地成独立的目标代码,读一段执行一段。启动快、跨平台省事,代价是每次运行都要重新分析一遍,这笔开销摊在每一次执行里。

四、字节码加虚拟机是两者的中间态。 Java 编译器 javac 生成的 .class 文件里不是机器码,是给 Java 虚拟机看的字节码。《Java 虚拟机规范》第 2 章说明,虚拟机是一台抽象的计算机,有自己的指令集、栈与运行时数据区。Python 也一样:CPython 先把源码编译成字节码,再逐条执行。所以「编译」这一步照样发生,只是产物不是最终答案。

五、即时编译把两条路合流。 JIT 的做法是运行时先解释,只把反复执行的热点代码编译成本地指令,兼顾启动速度与峰值性能。这也是「编译型一定比解释型快」站不住的原因:决定快慢的是实现有没有做优化,不是标签。

有争议:编译与解释的界线画在哪。 主流观点是这条线属于实现选择而不属于语言:《编译原理》第 1.2 节把两者放进同一套结构,《Crafting Interpreters》第 1 章也把两者统一称作「语言的实现」,同一门小语言两版实现都写得出来。也有观点认为界线仍有用:每次运行都要重新处理源码的程序,冷启动与部署方式确实不同,混为一谈会掩盖代价。本文用前者:判断一门语言快不快,要看实现做了什么。

边界条件:编译不等于「一次编译、到处运行」。 字节码仍然要由虚拟机解释或即时编译,虚拟机的实现因此对性能影响极大(《Java 虚拟机规范》第 2 章)。

编译器与解释器的对照:两者共用扫描、语法分析、语义分析这套前端,区别在后端——编译器生成目标机器码,解释器当场求值
编译器与解释器共用前端,省不掉的是分析,省得掉的是目标代码。

一个例子

在终端跑 python3 -c “import dis; dis.dis('a = 1 + b')”,屏幕上会出来一列字节码指令:把常量 1 压栈,把名字 b 的值压栈,做一次加法,把结果存进名字 a,最后返回。这就是《Python 标准库参考》里「dis —— Python 字节码反汇编器」做的事:它把 Python 悄悄做的那半步编译摊开给你看。

常见误解

  • 以为「解释型语言不编译」:CPython 每次运行都先把源码编译成字节码,只是产物不落地。
  • 以为编译型一定更快:JIT 能把热点代码优化到接近静态编译,快慢取决于实现。
  • 以为编译错误和运行错误是一回事:前者在程序开跑前就能发现,后者只在特定输入下暴露。
  • 以为「一次编译、到处运行」等于不用管平台:跨的是字节码,虚拟机仍要按平台各写一份。

应用场景

  • 生活:装 Python 或 Java 环境时,先看清缺的是运行时还是编译器,能少走一半弯路。
  • 职场:评估「换个语言重写」的收益时,把启动时间、峰值性能、部署复杂度分开问,别用「编译型/解释型」一句话打发。
  • 写作与创作:写程序员的角色,让他说清手里的错是编译期的还是运行期的,一句话就把专业性立住。

关联知识点

  • 编程思维(programming-mindset,先读它:怎么把问题拆成机器能执行的步骤)
  • 算法(algorithm,编译器的优化就是在步骤层面省力气)
  • 硬件与软件(hardware-software,编译的终点是硬件认识的那套指令)
  • 操作系统(operating-system,生成的可执行文件要靠它装载与调度)
  • 所属主线:语言与意义、因果与系统