在学习编译原理之前,有必要先了解源语言和目标语言之间的编译过程。
我们使用T型图来理解:在编译原理中,T型图(T-diagram) 是一种用于描述编译器结构和不同编程语言之间编译关系的图示方法。它被用来直观地表示一个编译器是如何将源代码从一种语言转换为另一种语言,特别是在 交叉编译(Cross Compilation) 或 引导编译(Bootstrapping) 中非常有用。
T型图通常由三部分组成:
- 输入语言(Source Language):要编译的源代码语言,通常写在T型图的左边。
- 编译程序实现语言(Host Language):即编译器自身的实现语言,进行转换作用,通常卸载T型图下边。
- 目标语言(Target Language):编译后的输出语言,通常写在T型图的右边。
如下图:
T型图左边的语言是源语言;右边的是目标语言;下方是编译程序实现语言。
在初步阶段,我们只需要知道T型图实现的是编译过程,同时如果将多个T型图拼接在一起,可以形成“滚雪球”的操作,称为自展,实现自编译过程。
那么我们为什么要介绍T型图呢?可以看见,编译过程就是将源语言转换目标语言,而对于转换的过程,我们往往会分成几个阶段(上期有讲),而再针对其中的词法分析、语法分析等,都是与一个语言的基本概念息息相关的。那么针对所有的高级语言,是否会有共同的特点,也就是通识存在,以便于我们更好地理解高级语言和编译过程呢?
有的,兄弟有的。这篇文章我们就来介绍其中的知识。
程序语言的定义
首先我们需要了解程序语言的定义,即它是如何来的?
首先我们需要知道的一个要点:程序设计教科书中的语言描述侧重于语言成分的意义,它常常只讲到语言的一部分,并且是浮于表面、基于使用的那一部分,是远远不能把这种描述称为构造编译程序的基础的。
什么意思呢?就拿我们现在所讲的中文、英语作比方:我们在学校学习的,往往都是它们的语法结构、语气以及声调等等,但我们一般不会去学习它是如何发声的,以及大脑是如何想出这种组织词汇变成一句话的;程序语言也一样,我们仅仅是对其语法和语义进行了解,但它是如何编译成我们想要的程序,中间过程是怎么样的,我们一般不会得知——实际上这也是学习编译原理的目的。
所以当我们理解程序语言的定义,实际上也仅仅是理解了其语法和语义,在描述完这一部分后,我们将会讲解它是如何变成计算机理解的语言,又是如何转变成为我们想要的程序。
词法和语法
任何语言程序都可看成是一i的那个的字符集上的字符串,但是如果仅仅是普通的字符串,例如以下这个字符串:
nsidonasonoin312ni n
是称不上一句话的。所以我们需要知道,构成一句合法的话,需要规则来制约语句的构成,这些规则的一部分称为词法规则。一部分称为语法规则。
词法规则
我们来举个例子:2*x+c
我们看这个语句,在词法的规则中,它会被分割为:2、x与c、*,它们一般分别会被称为常数、标识符以及运算符,这些被称为单词符号(Token),单词符号是由词法规则所确定的,词法规则规定了字母表中哪样的字符串是一个单词符号。
词法规则的不统一性
在不同的程序语言中,拥有不同的词法规则。
关键字
不同语言的关键字集合不同,例如:
-
C 语言
关键字:
int, char, if, else, while, for, return -
Python 语言
关键字:
def, class, if, else, elif, while, return -
JavaScript 语言
关键字:
function, var, let, const, if, else, return
标识符(变量、函数名等)
-
C/C++、Java、Python:标识符由 字母、数字、下划线
_组成,且不能以数字开头。int my_var = 10; // ✅ 合法 int 1var = 10; // ❌ 不合法 -
JavaScript 允许
$和@作为标识符:js复制编辑let $var = 10; // ✅ 合法 let _name = "Alice"; // ✅ 合法
字符串表示
不同语言的字符串表示方式不同:
-
C 语言:使用双引号 "
Python:支持单引号 ' 和双引号 ",以及三引号 '''
-
JavaScript:支持 模板字符串(反引号`)
注释
不同语言的注释格式不同:
-
C、C++、Java
// 单行注释 /* 多行 注释 */ -
Python
# 这是单行注释 ''' 这是多行注释 '''
数值表示
-
C 语言
允许二进制、八进制、十六进制:
int a = 42; // 十进制 int b = 0x2A; // 十六进制 int c = 052; // 八进制 -
Python
也支持多种数值格式:
a = 0b1010 # 二进制 b = 0o52 # 八进制 c = 0x2A # 十六进制
不同的词法规则构成了不同的程序语言,其中的原因是它们形成的逻辑不同。
语法规则
语法单位比单词符号更具有丰富的意义。语法就是一整个句子代表的意义。
例如:0.5*2+2,被称为一个算术式,具有通常的算术意义。
语言的规则定义了程序的形式结构,是判断输入字符串是否构成一个形式上正确程序的依据。我们可以这样理解:
语法即是使语言合法。
语义
如果说语法是使语言合法,那么语义就是使语言有意义。
也就是说,我们仅仅按照一定规则组成语句还不够,让人看懂,才是它存在的真正意义。
离开了语义,语言只不过是一堆符号的集合。使用语义可以定义一个程序的意义,语义关心的是代码的“意图”是什么,它应该如何运行,以及最终的计算结果是什么。
我们来讲语法和语义进行对比,就可以知道了。
语法 vs 语义:形象类比
自然语言中:
✅ 语法正确,语义正确:
- “小明吃了一个苹果。”(符合语法,语义清晰:小明真的吃了一个苹果。)
✅ 语法正确,但语义错误:
- “苹果吃了小明。”(语法没问题,但语义不符合常识,苹果不能吃人。)
❌ 语法错误,语义不明:
- “小明苹果吃了。”(语法混乱,虽然大致能猜到意思,但结构不标准。)
在编程中,同样的道理:
✅ 语法正确,语义正确
int x = 10 + 5; // 语法正确,语义正确,x 赋值为 15
✅ 语法正确,但语义错误
int x = 10 / 0; // 语法正确,但语义错误,除零错误
❌ 语法错误
int 5x = 10; // 语法错误,变量名不能以数字开头
所以可以见,
语法(Syntax) 就像是拼写规则,决定了一句话是否符合语言规范。
语义(Semantics) 则是句子的真正含义。
2. 语义的类型
程序的语义通常分为以下几种:
(1)操作语义(Operational Semantics)——“代码怎么执行”
核心思想:直接描述程序执行的步骤。
像是在执行菜谱,每一步明确告诉你要做什么。
例子
int x = 5 + 3;
操作语义:
- 计算
5 + 3,得到8 - 将
8赋值给变量x
(2)公理语义(Axiomatic Semantics)——“代码应该满足什么条件”
核心思想:用逻辑断言来描述程序执行前后的状态。
像是数学证明,用规则确保程序在执行前后保持某种性质。
📌 例子
int x = 5 + 3;
用逻辑公式描述:
- 前条件:
true(程序开始前不需要特殊假设) - 后条件:
x == 8(执行后,x 必须等于 8)
(3)指称语义(Denotational Semantics)——“代码对应的数学函数”
核心思想:把程序的行为转换成数学模型。
像是函数映射,代码是输入,语义是输出的数学定义。
📌 例子
int square(int x) {
return x * x;
}
数学定义:
square(x) = x²
这个函数的语义就是 square(3) = 9, square(4) = 16。
3. 形象解释语义的关键问题
(1)语法正确,但语义不符合预期
例子:
int x = 10;
int y = x / 0; // 除零错误
形象比喻:
语法层面:就像交通规则规定“红灯停,绿灯行”,这段代码的语法完全合法。
语义层面:但执行时,x / 0 没有意义,就像“在红灯亮时开车”一样,规则本身正确,但行为错误。
(2)语义的歧义
某些语言的语义可能存在歧义(Ambiguity),即相同的代码在不同情况下可能有不同的解释。
例子:Python 中的 +
print(5 + 3) # 8(整数相加)
print("5" + "3") # "53"(字符串拼接)
形象比喻: 就像 “跑” 这个词:
- 在田径比赛中,“跑”意味着冲刺。
- 在程序运行中,“跑”意味着执行。
- 在逃生场景中,“跑”意味着逃离危险。
同样,+ 在不同的情况下表现不同的语义。
4. 为什么语义重要?
(1)保证程序行为符合预期
程序员希望代码能按照预期运行,正确的语义定义可以避免错误。
例子
if (x = 5): # 这里 x = 5 是赋值,而不是条件判断
很多语言(如 C 语言)允许 if (x = 5) 语法正确,但语义不符合一般的期望,因此 Python 直接禁止了 if (x = 5) 这样的写法,以防止语义错误。
(2)静态分析和优化
编译器会利用语义信息进行优化:
int a = 5;
int b = a * 2;
如果 a 在后续代码中不变,编译器可以直接用 b = 10 代替 b = a * 2,提高执行效率。
(3)形式化验证
在安全性要求较高的系统(如航空、医疗软件)中,必须严格定义程序的语义,以保证代码不会导致灾难性错误。例如:
if (speed > MAX_SPEED) {
emergency_stop();
}
这个条件的语义要被严格验证,以防止飞机或医疗设备因为错误的代码逻辑导致故障。
总结
对于语法和语义,我们需要知道:
- 语法是形式,语义是意义,语法正确不代表语义正确。
- 语义描述程序的行为,可以用 操作语义、公理语义、指称语义 来解释。
- 语义的正确性至关重要,它决定了程序的正确性、优化和安全性。
写程序时,语法只是第一步,理解语义才能真正掌握编程的核心。
在理解了语法和语义之后,我们理解了程序语言的基本组成。
高级语言的一般特性
高级语言分类
从语言范型来对高级语言进行分类的话,有以下四种。
强制式语言
也称为过程式语言,特点是命令驱动,面向语句。程序由一系列按顺序执行的语句组成,程序的状态通过变量的赋值和修改进行改变。
典型语言: C、Pascal、Fortran、BASIC、Assembly
应用式语言
程序由数学上的函数(function)组成,每个函数接受输入并返回输出,且**不改变全局状态(无副作用)。与强制式语言相比,更重视程序所表示的功能,而不是一个语句接一个语句执行。
典型语言: Haskell、Lisp、Scheme、ML、Erlang、F#
基于规则的语言
其执行过程是:检查一定的条件,当它满足值,则执行适当的动作。即:条件->动作
典型语言: Prolog、CLIPS、Jess
面向对象语言
程序由对象(Object)组成,每个对象封装数据和方法,通过消息传递(Message Passing)进行交互。主要特征是:封装性、继承性、多态性 。以类(Class)和对象(Object)为核心。适用于大型软件开发,有良好的可复用性和可维护性。
典型语言: Java、C++、Python、C#、Ruby
包含的数据类型
1. 基本数据类型(Primitive Data Types)
这些是编程语言最基础的数据类型,通常直接由 CPU 寄存器或内存单元存储和操作。
(1)整数类型(Integer)
-
描述:用于存储整数(没有小数部分)。
-
常见类型:
int(整型)short(短整型)long(长整型)byte(字节型)
-
示例(Java):
int num = 100; long bigNum = 1000000000L;
(2)浮点类型(Floating-Point)
-
描述:用于存储带小数的数字,支持小数点表示和科学计数法。
-
常见类型:
float(单精度浮点数,通常 32 位)double(双精度浮点数,通常 64 位)
-
示例(Python):
pi = 3.14159 # 默认是 float
(3)字符类型(Character)
-
描述:用于存储单个字符,通常使用 Unicode 编码。
-
常见类型:
char(存储单个字符,如'A')
-
示例(C++):
char letter = 'A';
(4)布尔类型(Boolean)
-
描述:用于存储逻辑值
true或false。 -
示例(JavaScript):
let isDone = true;
2. 复合数据类型(Composite Data Types)
这些类型用于存储多个数据元素,可以是同类型或不同类型的组合。
(1)数组(Array)
-
描述:存储相同类型的多个元素,按索引访问。
-
示例(Java):
int[] numbers = {1, 2, 3, 4, 5};
(2)字符串(String)
-
描述:存储一系列字符,通常被视为不可变对象。
-
示例(Python):
name = "Alice"
(3)结构体(Struct)
-
描述:将多个不同类型的数据组合在一起(主要用于 C 语言)。
-
示例(C):
struct Person { char name[50]; int age; };
(4)元组(Tuple)
-
描述:存储多个不同类型的数据,长度固定(主要用于 Python)。
-
示例(Python):
person = ("Alice", 25, True)
(5)枚举(Enum)
-
描述:定义一组命名的常量,表示有限的离散值集合。
-
示例(C#):
enum Day { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday };
3. 抽象数据类型(Abstract Data Types,ADT)
这些类型在高级语言中通常通过类(Class)或接口(Interface)来实现。
(1)列表(List)
-
描述:动态数组,可变长度。
-
示例(Python):
my_list = [1, 2, 3, 4]
(2)集合(Set)
-
描述:存储唯一元素,无序集合。
-
示例(Java):
Set<Integer> set = new HashSet<>(); set.add(10);
(3)映射(Map/Dictionary)
-
描述:存储键值对(Key-Value)。
-
示例(JavaScript):
let person = {name: "Alice", age: 25};
(4)栈(Stack)
-
描述:后进先出(LIFO)的数据结构。
-
示例(Java):
Stack<Integer> stack = new Stack<>(); stack.push(10);
(5)队列(Queue)
-
描述:先进先出(FIFO)的数据结构。
-
示例(Python):
from collections import deque queue = deque([1, 2, 3])
4. 指针类型(Pointer)
-
描述:存储内存地址,通常用于 C/C++。
-
示例(C++):
int num = 10; int *ptr = # // 指针存储变量的地址
5. 用户自定义数据类型
-
描述:在面向对象编程(OOP)中,用户可以定义自己的数据类型(类)。
-
示例(Java):
class Person { String name; int age; }
总结
| 数据类型 | 描述 | 例子 |
|---|---|---|
| 基本类型 | 整数、浮点数、字符、布尔 | int, float, char, boolean |
| 复合类型 | 数组、字符串、结构体、枚举、元组 | String, Array, struct |
| 抽象数据类型 | 列表、集合、映射、栈、队列 | List, Set, Map, Stack, Queue |
| 指针类型 | 存储内存地址 | int* |
| 用户自定义类型 | 类、接口 | class Person {} |
不同编程语言可能有额外的数据类型,如 Python 的 complex(复数)或 JavaScript 的 Symbol,但以上是大多数语言通用的数据类型。
程序语言的语法描述
程序语言的语法(Syntax)是指该语言的结构规则,定义了如何编写合法的程序代码。语法描述通常包括**词法规则、语法规则,并使用形式化方法(如BNF、EBNF)进行定义。
1. 语法的层次结构
程序语言的语法可以分为以下三个层次:
(1)词法规则(Lexical Syntax)
-
词法规则定义了最小的有效单元(Token),即关键字、标识符、操作符、常量等。
-
例如,在
int x = 10;中,Token 包括:
int(关键字)x(标识符)=(赋值运算符)10(整数常量);(分号)
词法分析(Lexical Analysis)由**词法分析器(Lexer 或 Scanner)**完成,它会将源代码拆分成 Token 序列。
-
示例(C 语言整数定义的词法规则)
digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" integer ::= digit {digit}
(2)语法规则(Syntactic Rules)
-
语法规则描述了Token 如何组成有效的语句。
-
例如,赋值语句的语法:
assignment ::= identifier "=" expression ";"- 这表示赋值语句由标识符(变量)、
=、表达式和;组成。
- 这表示赋值语句由标识符(变量)、
**语法分析(Parsing)是由语法分析器(Parser)**完成的,它负责检查 Token 是否按照正确的语法组织。
-
示例(BNF 定义 C 语言的 if 语句)
if-statement ::= "if" "(" expression ")" statement [ "else" statement ][]表示可选部分- 该规则说明:
if关键字后面必须有()()内部必须是表达式expressionif语句后面必须有statementelse语句是可选的([...])
(3)语义规则(Semantic Rules)
-
语义规则定义了代码的意义,即程序的行为。
-
例如,
x = 10 / 0;语法是正确的,但语义错误(除零错误)。 -
示例(类型检查)
int x = "hello"; // 语法正确,但类型错误 -
示例(作用域规则)
{ int a = 10; } printf("%d", a); // 语法正确,但变量 a 超出作用域
2. 形式化语法描述方法
为了精确定义编程语言的语法,通常使用形式化方法进行描述,其中常见的方法包括:
(1)BNF(巴科斯范式,Backus-Naur Form)
BNF 是一种用递归规则表示语法的方式,例如:
<expr> ::= <term> | <expr> "+" <term>
<term> ::= <factor> | <term> "*" <factor>
<factor> ::= "(" <expr> ")" | <number>
-
::=代表“定义为” -
|代表“或” -
这定义了一个
加法和乘法的表达式
:
<expr>由<term>组成,或者由<expr> + <term>组成<term>由<factor>组成,或者由<term> * <factor>组成<factor>可以是括号中的表达式,或者是一个数字
例如:
3 + 4符合语法(<expr> ::= <term> + <term>)(5 * 2) + 10也符合语法
(2)EBNF(扩展巴科斯范式,Extended Backus-Naur Form)
EBNF 在 BNF 基础上增加了可选项、重复项等增强功能:
expression ::= term { ("+" | "-") term }
term ::= factor { ("*" | "/") factor }
factor ::= number | "(" expression ")"
{...}代表重复 0 次或多次(...)代表分组|代表或
例如:
-
5 * 3 + 8 / 2解析过程:
expression → term { ("+" | "-") term } → factor { ("*" | "/") factor } { ("+" | "-") term } → number { ("*" | "/") factor } { ("+" | "-") term }
(3)语法图(Syntax Diagram)
语法图是一种可视化的语法表示方式,适用于 EBNF 规则,例如:
📌 表达式 expression ::= term { ("+" | "-") term }
[term] → (+ or -) → [term] → (+ or -) → [term] → ...
[term]是操作数(+ or -)代表可以是+或-- 箭头
→表示顺序
类似于流程图,适合图形化工具解析。
3. 语法解析(Parsing)
语法解析器(Parser)用于检查代码是否符合语法规则,分为**自顶向下(Top-Down)和自底向上(Bottom-Up)**两种。
(1)自顶向下解析(Top-Down Parsing)
- 递归下降解析(Recursive Descent Parsing)
- 适用于 LL 语法(从左到右扫描,最左推导)
- 例如
if (condition) { statement } else { statement }
(2)自底向上解析(Bottom-Up Parsing)
- LR 解析(Left-to-Right, Rightmost Derivation)
- 适用于复杂语言,如 C、Java
- 先扫描 Token,逐步构造语法树
总结
| 层次 | 作用 | 示例 |
|---|---|---|
| 词法规则 | 定义 Token(关键字、标识符、运算符) | int x = 10; |
| 语法规则 | 定义 Token 如何组成语句 | if (x > 0) { ... } |
| 语义规则 | 定义程序的含义(类型检查、作用域) | 10 / 0(除零错误) |
| BNF 语法 | 用递归定义语法 | ` ::= |
| EBNF 语法 | 增强 BNF,支持可选和重复 | `term ::= factor { ("*" |
| 语法图 | 直观可视化语法 | expression → term → {(+ or -) term} |
程序语言的语法描述对于编译器设计、解析器开发至关重要,它确保代码的正确性,并提供基础工具来处理各种编程语言的结构。
好的,现在你已经大致了解了高级语言的一般知识了,然而,我们学习的这门课叫做编译原理,也就是说,你需要知道的是,如果将高级语言转化成计算机听得懂的东西,不知道这一步,说再多也是浮云。
就好比你和一个外国人讲话,互相听不懂各自的语言,还怎么交流?这时候,就需要翻译了——也就是编译器。
那么从下一章开始,就会正式进入编译原理的大门。