软考软件设计师上午题中,编译原理每年必出两到三道,其中编译器各阶段的功能划分与输入输出关系是最高频考点。不少考生靠死记硬背"词法→语法→语义→中间代码→优化→目标代码"这个顺序,但一到具体题目——类型检查归哪个阶段、括号不配对在哪一步报错、记号流是谁的输出——立刻掉进命题人的坑。本文从编译流水线的完整链路出发,逐阶段拆解技术机制与软考命题套路。
编译器本质上是一种翻译程序,其任务是将用高级程序设计语言书写的源程序完整地转换为功能等价的低级目标程序,通常是汇编语言程序或机器语言程序。这一转换过程不是一步完成的,而是通过多个前后衔接的处理阶段,逐步降低语言抽象层次,最终生成可在目标机器上执行的代码。
与编译器形成对照的是解释器。编译器采用"先翻译后执行"的工作方式,将源程序整体翻译为目标代码后再运行,用户程序运行效率高但可移植性差——生成的目标代码与特定硬件平台绑定。解释器则"边翻译边执行",逐条读取源程序语句,翻译一条执行一条,不产生独立的目标代码文件,运行效率低但可移植性好。Java语言采用的即时编译技术结合了两者特点,先将源程序编译为平台无关的字节码,运行时再通过即时编译器将热点字节码动态编译为本地机器码。
理解编译器与解释器的区别,是软考中辨析编译型语言与解释型语言实现机制的起点。编译器处理的典型语言包括C语言、C++、Fortran等,解释器处理的典型语言包括早期的Basic、Python脚本模式、JavaScript传统引擎等。在软考命题中,关于编译器工作方式的正确表述是"先翻译后执行,用户程序运行效率高但可移植性差"。
在深入编译器内部阶段之前,有必要先厘清一个更大的概念框架。将一份C语言源程序变成可以运行的可执行文件,实际上要经历四个步骤:预处理、编译、汇编、链接。
预处理阶段处理源文件中的宏定义展开、头文件插入和条件编译指令判断,将源程序展开为纯净代码。编译阶段是本文论述的核心——将高级语言源程序翻译为汇编语言程序,内部又分为词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成等子阶段。汇编阶段由汇编器将汇编代码翻译为可重定位的机器语言目标模块。链接阶段由链接器将目标模块与库函数拼接为完整可执行文件,完成符号解析和重定位。软考曾多次考查这四个步骤的正确顺序,常见干扰项是将汇编与编译位置互换。
词法分析是编译器工作的第一个阶段,其输入是源程序的字符序列,输出是记号流。词法分析器逐字符扫描源程序文本,依据语言的词法规则,将字符序列切分为一个个有独立含义的词法单元,即记号。每个记号通常包含记号类别和属性值两部分信息,例如对于标识符"count",其类别为标识符,属性值为指向符号表中对应条目的指针。
词法分析器的理论基础是正规式与有限自动机。正规式用于描述程序设计语言中各类单词的构词规则,例如标识符通常定义为以字母或下划线开头、后跟字母数字下划线组成的字符串;整数常量定义为数字序列。根据正规式可以构造出确定有限自动机或非确定有限自动机,作为词法分析器的实现模型。给定一个正规式,存在与之等价的确定有限自动机,这是词法分析器可以机械实现的数学保证。
在软考中,词法分析阶段的典型考查方式包括:判断某个错误应该由哪个阶段报告。非法字符——即源程序中出现了语言字母表之外的字符——在词法分析阶段就会被检测并报告错误,因为这个阶段的工作就是逐个字符地识别单词,遇到无法归类为任何记号类型的字符序列时,词法分析器立即报错。需要特别注意,括号不匹配不是词法层面的错误,括号本身是合法的单个字符记号,其匹配关系属于语法结构问题。
语法分析以词法分析输出的记号流作为输入,根据程序设计语言的语法规则,将线性的记号序列组织为反映程序语法结构的树形表示——语法树。语法分析器验证记号序列是否构成一个合乎语法的程序,并在验证过程中显式或隐式地构建程序的层次结构。
语法分析的理论基础是上下文无关文法。上下文无关文法由终结符集合、非终结符集合、产生式集合和开始符号四个要素组成,其产生式左部总是单个非终结符,替换不依赖于上下文。程序设计语言的绝大多数语法结构——表达式、语句、函数定义、类声明等——都可以用上下文无关文法精确描述。乔姆斯基将形式文法分为四种类型,其中程序设计语言的大多数语法现象属于上下文无关文法,仅有少数特性(如变量先声明后使用)需要上下文有关文法的描述能力。
语法分析方法分为两大类:自上而下分析和自下而上分析。自上而下分析从文法的开始符号出发,尝试逐步推导出输入记号串,递归子程序分析法就是自上而下分析的典型代表——为每个非终结符编写一个递归子程序,通过子程序之间的相互递归调用来完成语法分析。自下而上分析则从输入的记号串出发,逐步归约到文法的开始符号,常见的移进归约分析法和算符优先分析法都属于这一类。
软考对语法分析的考查重点之一,是明确哪些错误由语法分析阶段报告。括号不配对、关键字拼写在语法位置上的误用、表达式缺少操作数、语句末尾缺少分号等都属于语法错误,由语法分析器检测。在语义上正确但语法结构不合规则的代码——例如将关键字用作标识符——也在这一阶段被拦截。另一个常考角度是逆波兰表示法与语法树的关系:给定一个表达式,画出其语法树后,对语法树进行后序遍历即可得到该表达式的后缀式。
语义分析是编译器前端与后端的衔接环节,其输入是语法分析阶段产生的语法树,输出是经过语义检查并填充了类型信息的带标注语法树。语义分析的主要任务包括类型检查和上下文约束验证两大方面。
类型检查是语义分析阶段最核心的工作。编译器遍历语法树的各个节点,检查每个运算符的操作数类型是否兼容、函数调用的实参与形参类型是否匹配、赋值语句左右两侧的类型是否兼容、数组下标是否为整型等。软考曾直接以"类型检查在哪个阶段处理"为题设问,正确答案是语义分析阶段。很多考生看到"分析"二字就联想到语法分析,但语法分析只关心结构的形式是否合规,不对类型的合法性做判断。
语义分析还负责检查程序中那些语法正确但语义不合逻辑的情况——例如在使用前未声明的变量、重复声明的标识符、函数返回值类型与声明不一致、break语句出现在循环或switch之外等。这些问题的共同特点是:按照语法规则,代码的形式完全合法,按照语义规则却存在缺陷。
中间代码生成是编译器从分析阶段转入综合阶段的过渡步骤。中间代码是一种介于高级语言与目标机器语言之间的抽象表示形式,其设计目标是在足够接近机器语言以便于后续优化的同时,保持与具体目标机器的无关性。常用的中间代码形式包括三地址代码、四元式、逆波兰表示和抽象语法树等。三地址代码将每个复杂表达式分解为一系列最多含一个操作符的简单赋值语句,形如x等于y运算符z,便于后续的代码优化和目标代码生成。四元式由操作符、第一操作数、第二操作数和结果四个域组成,是三地址代码的结构化表示。
代码优化阶段以中间代码为输入,在不改变程序语义的前提下,对中间代码进行等价变换,以提高目标程序的运行效率或减小目标程序的规模。优化可以在不同范围内进行:局部优化局限于单个基本块内部,主要技术包括常量折叠、公共子表达式消除、复写传播和死代码删除;循环优化针对程序中执行频率最高的循环体,采用代码外提、强度削弱和归纳变量消除等技术降低循环开销;全局优化跨越基本块边界,进行全局数据流分析以实现更广泛的优化。
以常量折叠为例,源程序中表达式3加5乘以2在编译时即可算出13并替换,避免运行时重复计算。以强度削弱为例,循环体内i
本篇完!