编译器概述
编译器是一个将高级语言(如C++、Java)源程序翻译成等价的低级语言(如汇编语言、机器语言)目标程序的程序,整个过程通常分为两个主要阶段:

- 前端:与源语言相关,主要包括词法分析、语法分析、语义分析。
- 后端:与目标机相关,主要包括中间代码生成、代码优化、目标代码生成。
我们主要关注前端和中间代码生成,并用C语言实现一个简单的计算器,它能解析类似 3 + 5 * (10 - 4) 这样的表达式并计算结果。
第一阶段:词法分析
词法分析器的任务是读取源代码字符流,将其转换成一系列有意义的“单词”,称为记号。
核心概念
- 词法单元:源程序中的最小语法单元。
int,x, ,10, 都是词法单元。 - 记号:词法单元的内部表示,通常包含一个类型和一个值(可选)。
"123"这个字符串会被识别为数字记号,其类型为NUM,值为123。
- 正则表达式:用来描述词法单元的模式,数字可以用
[0-9]+描述。
C语言实现
我们将为我们的计算器定义几种记号类型。
定义记号结构体

// 词法单元类型
typedef enum {
// 特殊记号
END_OF_FILE, // 文件结束
ERROR, // 错误
// 操作符
PLUS, // +
MINUS, // -
MULTIPLY, // *
DIVIDE, // /
LPAREN, // (
RPAREN, // )
// 操作数
NUMBER // 数字
} TokenType;
// 记号结构体
typedef struct {
TokenType type; // 记号类型
int value; // 记号对应的值 (主要用于数字)
char lexeme[32]; // 记号在源代码中的原始字符串 (可选,用于调试)
} Token;
实现词法分析器
一个简单的词法分析器,逐个字符读取输入,根据字符类型生成记号。
#include <stdio.h>
#include <ctype.h>
#include <string.h>
// 假设我们有一个全局的输入指针和当前记号
const char* input;
Token current_token;
// 跳过空白字符 (空格, tab, 换行)
void skip_whitespace() {
while (isspace(*input)) {
input++;
}
}
// 词法分析主函数
void get_next_token() {
skip_whitespace();
if (*input == '\0') {
current_token.type = END_OF_FILE;
return;
}
// 处理数字
if (isdigit(*input)) {
int val = 0;
while (isdigit(*input)) {
val = val * 10 + (*input - '0');
input++;
}
current_token.type = NUMBER;
current_token.value = val;
return;
}
// 处理操作符和括号
switch (*input) {
case '+': current_token.type = PLUS; input++; break;
case '-': current_token.type = MINUS; input++; break;
case '*': current_token.type = MULTIPLY; input++; break;
case '/': current_token.type = DIVIDE; input++; break;
case '(': current_token.type = LPAREN; input++; break;
case ')': current_token.type = RPAREN; input++; break;
default:
// 无法识别的字符
current_token.type = ERROR;
input++;
break;
}
}
第二阶段:语法分析
语法分析器的任务是根据语法规则(通常用上下文无关文法CFG描述),将记号流组织成一个树形结构,称为抽象语法树,AST比复杂的上下文无关文法树更简洁,它忽略了语法细节(如括号),只保留了程序的逻辑结构。
核心概念
- 上下文无关文法:用于描述编程语言语法的一套规则,一个简单的表达式文法可以是:
Expr -> Term Expr' Expr' -> + Term Expr' | - Term Expr' | ε Term -> Factor Term' Term' -> * Factor Term' | / Factor Term' | ε Factor -> ( Expr ) | NUMBER这里的 (epsilon) 表示“空”或“可以省略”,这种递归下降的写法非常适合用代码实现。
- 抽象语法树:AST的节点代表程序结构,对于
3 + 5,AST会是:+ / \ 3 5
C语言实现
定义AST节点类型
我们需要定义不同类型的节点来表示表达式中的不同部分。
// AST 节点类型
typedef enum {
NODE_NUMBER,
NODE_PLUS,
NODE_MINUS,
NODE_MULTIPLY,
NODE_DIVIDE
} NodeType;
// AST 节点结构体
typedef struct ASTNode {
NodeType type;
int value; // 如果是数字节点,存储其值
// 如果是二元操作符,左右子节点
struct ASTNode* left;
struct ASTNode* right;
} ASTNode;
// 创建数字节点
ASTNode* make_number_node(int value) {
ASTNode* node = (ASTNode*)malloc(sizeof(ASTNode));
node->type = NODE_NUMBER;
node->value = value;
node->left = node->right = NULL;
return node;
}
// 创建二元操作符节点
ASTNode* make_binary_node(NodeType type, ASTNode* left, ASTNode* right) {
ASTNode* node = (ASTNode*)malloc(sizeof(ASTNode));
node->type = type;
node->left = left;
node->right = right;
return node;
}
实现递归下降语法分析器
这个分析器会根据文法规则,逐个匹配记号并构建AST。
// 前向声明,因为函数之间会相互调用
ASTNode* parse_expr();
ASTNode* parse_term();
ASTNode* parse_factor();
// 匹配当前记号,如果匹配成功则获取下一个记号
void match(TokenType expected_type) {
if (current_token.type == expected_type) {
get_next_token();
} else {
printf("Syntax Error: Expected %d, but got %d\n", expected_type, current_token.type);
exit(1);
}
}
// 解析 Factor -> ( Expr ) | NUMBER
ASTNode* parse_factor() {
ASTNode* node;
if (current_token.type == NUMBER) {
node = make_number_node(current_token.value);
match(NUMBER);
} else if (current_token.type == LPAREN) {
match(LPAREN);
node = parse_expr(); // 递归解析括号内的表达式
match(RPAREN);
} else {
printf("Syntax Error: Expected NUMBER or (, but got %d\n", current_token.type);
exit(1);
}
return node;
}
// 解析 Term -> Factor Term'
// Term' -> * Factor Term' | / Factor Term' | ε
ASTNode* parse_term() {
ASTNode* node = parse_factor();
while (current_token.type == MULTIPLY || current_token.type == DIVIDE) {
TokenType op = current_token.type;
match(op);
ASTNode* right = parse_factor();
node = make_binary_node(op, node, right);
}
return node;
}
// 解析 Expr -> Term Expr'
// Expr' -> + Term Expr' | - Term Expr' | ε
ASTNode* parse_expr() {
ASTNode* node = parse_term();
while (current_token.type == PLUS || current_token.type == MINUS) {
TokenType op = current_token.type;
match(op);
ASTNode* right = parse_term();
node = make_binary_node(op, node, right);
}
return node;
}
第三阶段:语义分析与中间代码生成
对于我们的简单计算器,语义分析主要是检查类型是否匹配(不能对字符串做加法),但在我们的例子中,所有操作都是数字,所以可以跳过。
中间代码生成是将AST转换成一种与机器无关的、简单的指令序列,常用的中间代码有三地址码,形如 x = y op z。
C语言实现
我们可以通过遍历AST来生成三地址码,这里我们简化处理,直接解释执行AST来得到结果,这本质上就是一种最简单的中间代码(树形中间代码)的执行。
解释执行AST
// 解释执行AST并返回计算结果
int evaluate(ASTNode* node) {
if (node == NULL) {
return 0;
}
// 如果是数字节点,直接返回其值
if (node->type == NODE_NUMBER) {
return node->value;
}
// 递归计算左右子节点的值
int left_val = evaluate(node->left);
int right_val = evaluate(node->right);
// 根据操作符类型执行相应运算
switch (node->type) {
case NODE_PLUS: return left_val + right_val;
case NODE_MINUS: return left_val - right_val;
case NODE_MULTIPLY: return left_val * right_val;
case NODE_DIVIDE: return left_val / right_val; // 注意:这里没有处理除零错误
default:
printf("Error: Unknown node type %d\n", node->type);
exit(1);
}
}
第四阶段:主程序 - 整合所有部分
我们将所有部分组合起来,创建一个完整的程序。
#include <stdlib.h> // for malloc
// 函数声明
void get_next_token();
void match(TokenType expected_type);
ASTNode* parse_expr();
ASTNode* parse_term();
ASTNode* parse_factor();
int evaluate(ASTNode* node);
void free_ast(ASTNode* node); // 添加一个释放AST内存的函数
void free_ast(ASTNode* node) {
if (node == NULL) return;
free_ast(node->left);
free_ast(node->right);
free(node);
}
int main() {
// 示例输入
input = "3 + 5 * (10 - 4)";
printf("Input: %s\n", input);
// 初始化第一个记号
get_next_token();
// 开始语法分析,构建AST
ASTNode* ast = parse_expr();
// 检查是否在表达式结束后还有多余字符
if (current_token.type != END_OF_FILE) {
printf("Syntax Error: Unexpected token at end of input\n");
free_ast(ast);
return 1;
}
// 解释执行AST并打印结果
int result = evaluate(ast);
printf("Result: %d\n", result);
// 释放AST占用的内存
free_ast(ast);
return 0;
}
这个C语言示例清晰地展示了《现代编译原理》的核心思想:
- 词法分析:将字符流转换为记号流,我们用
enum定义记号类型,用get_next_token()函数实现。 - 语法分析:根据文法规则,将记号流构建成结构化的AST,我们用递归下降分析法,将文法规则直接翻译成递归函数 (
parse_expr,parse_term,parse_factor)。 - 语义分析与中间代码生成/执行:我们对AST进行遍历(后序遍历),在遍历过程中进行计算,这既是语义检查(隐式的,因为我们只支持数字运算),也是中间代码(AST)的解释执行。
编译器后端(代码优化、目标代码生成)的过程会更加复杂,需要考虑目标机器的指令集、寄存器分配、寻址方式等,但它接收的输入通常就是这个阶段的中间代码(如三地址码),理解前端是迈向构建完整编译器的关键一步。
