02-预处理与词法分析¶
重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐ 学习时间:2天 必须掌握:是
为什么学这一章?¶
预处理和词法分析是编译的最前端阶段。预处理器处理宏替换、文件包含等指令,而词法分析器(Lexer)将源代码字符流转化为有意义的记号(Token)序列。
学完这一章,你将能够: - ✅ 掌握C/C++预处理器的完整工作流程 - ✅ 理解词法分析的原理与正则表达式 - ✅ 手写一个简单的词法分析器 - ✅ 理解有限自动机在词法分析中的应用
📖 核心概念¶
1. 预处理阶段¶
C/C++编译器在真正编译之前,会先执行预处理。预处理器是一个文本替换工具。
┌─────────────────────────────────────────────────────────────┐
│ 预处理器的工作 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 源文件 (.c/.cpp) │
│ │ │
│ ├── #include 处理 → 展开头文件内容 │
│ ├── #define 处理 → 宏替换 │
│ ├── #ifdef 处理 → 条件编译 │
│ ├── #pragma 处理 → 编译器指令 │
│ └── 注释删除 → 去除所有注释 │
│ │ │
│ ▼ │
│ 预处理后文件 (.i) │
│ │
└─────────────────────────────────────────────────────────────┘
实验:观察预处理输出¶
// preprocess_demo.c
#include <stdio.h>
#define MAX_SIZE 100
#define SQUARE(x) ((x) * (x))
#ifdef DEBUG
#define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
#define LOG(msg)
#endif
int main() {
int arr[MAX_SIZE];
int result = SQUARE(5 + 1);
LOG("程序开始");
printf("result = %d\n", result);
return 0;
}
# 只执行预处理,输出到 .i 文件
gcc -E preprocess_demo.c -o preprocess_demo.i
# 观察宏展开结果
# SQUARE(5+1) → ((5+1) * (5+1)) = 36,而非 5+1*5+1 = 11
宏的常见陷阱¶
// 宏陷阱示例
#include <stdio.h>
// 陷阱1:缺少括号的宏
#define BAD_SQUARE(x) x * x
#define GOOD_SQUARE(x) ((x) * (x))
// 陷阱2:带副作用的宏参数
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() {
// 陷阱1演示
printf("BAD: %d\n", BAD_SQUARE(3 + 1)); // 3+1*3+1 = 7,不是16
printf("GOOD: %d\n", GOOD_SQUARE(3 + 1)); // ((3+1)*(3+1)) = 16
// 陷阱2演示
int a = 5, b = 3;
int m = MAX(a++, b++);
// a++被执行了两次!结果不可预期
printf("a=%d, b=%d, max=%d\n", a, b, m);
return 0;
}
2. 词法分析概述¶
词法分析器(Lexer / Scanner / Tokenizer)的任务是:将字符流转换为记号(Token)流。
┌─────────────────────────────────────────────────────────────┐
│ 词法分析过程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 输入字符流: i n t m a i n ( ) { \n │
│ │
│ 输出Token流: │
│ ┌──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ KEYWORD │ IDENT │ LPAREN │ RPAREN │ LBRACE │ │
│ │ "int" │ "main" │ "(" │ ")" │ "{" │ │
│ └──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Token的分类¶
| Token类型 | 示例 | 说明 |
|---|---|---|
| 关键字(Keyword) | int, if, return | 语言保留字 |
| 标识符(Identifier) | main, count, x | 用户定义的名字 |
| 字面量(Literal) | 42, 3.14, "hello" | 常量值 |
| 运算符(Operator) | +, -, ==, && | 运算符号 |
| 分隔符(Delimiter) | (, ), {, ; | 分隔符号 |
3. 正则表达式与Token定义¶
编译器使用正则表达式来定义每种Token的模式:
标识符: [a-zA-Z_][a-zA-Z0-9_]*
整数: [0-9]+
浮点数: [0-9]+\.[0-9]+
字符串: "([^"\\]|\\.)*"
运算符: \+|\-|\*|\/|==|!=|<=|>=
4. 有限自动机(DFA/NFA)¶
词法分析器的本质是一个有限状态自动机:
┌─────────────────────────────────────────────────────────────┐
│ 识别标识符的DFA状态转换图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 字母/下划线 字母/数字/下划线 │
│ ┌───┐ ──────────→ ┌───┐ ──────────→ ┌───┐ │
│ │ S │ │ A │ (循环) │ A │ │
│ └───┘ └───┘ ←────────── └───┘ │
│ 起始状态 接受状态 │
│ │
│ S:初始状态 │
│ A:已读取至少一个字母/下划线,为接受状态 │
│ 遇到非字母数字下划线字符时停止,输出Token │
│ │
└─────────────────────────────────────────────────────────────┘
5. 手写词法分析器¶
下面用C语言实现一个简单但完整的词法分析器:
// simple_lexer.c - 简单词法分析器
#include <stdio.h> // 引入头文件
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// Token类型枚举
typedef enum {
TOKEN_INT, // int关键字
TOKEN_RETURN, // return关键字
TOKEN_IF, // if关键字
TOKEN_ELSE, // else关键字
TOKEN_IDENTIFIER, // 标识符
TOKEN_NUMBER, // 数字
TOKEN_PLUS, // +
TOKEN_MINUS, // -
TOKEN_STAR, // *
TOKEN_SLASH, // /
TOKEN_ASSIGN, // =
TOKEN_EQ, // ==
TOKEN_LPAREN, // (
TOKEN_RPAREN, // )
TOKEN_LBRACE, // {
TOKEN_RBRACE, // }
TOKEN_SEMICOLON, // ;
TOKEN_EOF, // 文件结束
TOKEN_UNKNOWN // 未知
} TokenType;
// Token结构
typedef struct {
TokenType type;
char value[256];
int line;
int column;
} Token;
// 词法分析器状态
typedef struct {
const char *source; // 指针:存储变量的内存地址
int pos;
int line;
int column;
} Lexer;
// 关键字表
static const char *keywords[] = {"int", "return", "if", "else"};
static const TokenType keyword_types[] = {
TOKEN_INT, TOKEN_RETURN, TOKEN_IF, TOKEN_ELSE
};
#define NUM_KEYWORDS 4
// 初始化词法分析器
void lexer_init(Lexer *lex, const char *source) {
lex->source = source;
lex->pos = 0;
lex->line = 1;
lex->column = 1;
}
// 获取当前字符
char current(Lexer *lex) {
return lex->source[lex->pos];
}
// 前进一个字符
void advance(Lexer *lex) {
if (current(lex) == '\n') {
lex->line++;
lex->column = 1;
} else {
lex->column++;
}
lex->pos++;
}
// 跳过空白字符和注释
void skip_whitespace(Lexer *lex) {
while (current(lex)) {
if (isspace(current(lex))) {
advance(lex);
} else if (current(lex) == '/' && lex->source[lex->pos + 1] == '/') {
// 单行注释
while (current(lex) && current(lex) != '\n') advance(lex);
} else {
break;
}
}
}
// 读取标识符或关键字
Token read_identifier(Lexer *lex) {
Token tok;
tok.line = lex->line;
tok.column = lex->column;
int start = lex->pos;
while (isalnum(current(lex)) || current(lex) == '_') {
advance(lex);
}
int len = lex->pos - start;
strncpy(tok.value, lex->source + start, len);
tok.value[len] = '\0';
// 检查是否为关键字
tok.type = TOKEN_IDENTIFIER;
for (int i = 0; i < NUM_KEYWORDS; i++) {
if (strcmp(tok.value, keywords[i]) == 0) {
tok.type = keyword_types[i];
break;
}
}
return tok;
}
// 读取数字
Token read_number(Lexer *lex) {
Token tok;
tok.type = TOKEN_NUMBER;
tok.line = lex->line;
tok.column = lex->column;
int start = lex->pos;
while (isdigit(current(lex))) advance(lex);
int len = lex->pos - start;
strncpy(tok.value, lex->source + start, len);
tok.value[len] = '\0';
return tok;
}
// 获取下一个Token
Token next_token(Lexer *lex) {
skip_whitespace(lex);
Token tok;
tok.line = lex->line;
tok.column = lex->column;
char c = current(lex);
if (c == '\0') { tok.type = TOKEN_EOF; strcpy(tok.value, "EOF"); return tok; }
if (isalpha(c) || c == '_') return read_identifier(lex);
if (isdigit(c)) return read_number(lex);
advance(lex);
tok.value[0] = c; tok.value[1] = '\0';
switch (c) {
case '+': tok.type = TOKEN_PLUS; break;
case '-': tok.type = TOKEN_MINUS; break;
case '*': tok.type = TOKEN_STAR; break;
case '/': tok.type = TOKEN_SLASH; break;
case '(': tok.type = TOKEN_LPAREN; break;
case ')': tok.type = TOKEN_RPAREN; break;
case '{': tok.type = TOKEN_LBRACE; break;
case '}': tok.type = TOKEN_RBRACE; break;
case ';': tok.type = TOKEN_SEMICOLON; break;
case '=':
if (current(lex) == '=') {
advance(lex);
tok.type = TOKEN_EQ;
strcpy(tok.value, "==");
} else {
tok.type = TOKEN_ASSIGN;
}
break;
default: tok.type = TOKEN_UNKNOWN; break;
}
return tok;
}
// 测试主函数
int main() {
const char *code = "int main() {\n"
" int x = 42;\n"
" return x + 1;\n"
"}\n";
printf("源代码:\n%s\n", code);
printf("Token序列:\n");
printf("%-4s %-16s %-14s %s\n", "行", "类型", "值", "位置");
printf("──────────────────────────────────────────\n");
Lexer lex;
lexer_init(&lex, code);
Token tok;
do {
tok = next_token(&lex);
printf("%-4d %-16d %-14s (%d:%d)\n",
tok.line, tok.type, tok.value, tok.line, tok.column);
} while (tok.type != TOKEN_EOF);
return 0;
}
🔍 深入理解¶
Flex/Lex工具¶
工业级编译器通常使用词法分析器生成工具:
/* simple.l - Flex词法规则文件 */
%{
#include <stdio.h>
%}
%%
"int" { printf("KEYWORD: %s\n", yytext); }
"return" { printf("KEYWORD: %s\n", yytext); }
[a-zA-Z_][a-zA-Z0-9_]* { printf("IDENT: %s\n", yytext); }
[0-9]+ { printf("NUMBER: %s\n", yytext); }
"+" { printf("PLUS\n"); }
"=" { printf("ASSIGN\n"); }
"==" { printf("EQ\n"); }
";" { printf("SEMICOLON\n"); }
[ \t\n] { /* 跳过空白 */ }
. { printf("UNKNOWN: %s\n", yytext); }
%%
int main() { yylex(); return 0; }
int yywrap() { return 1; }
💡 面试常见问题¶
Q1:预处理器中 #define 和 const 的区别?¶
答:#define 是文本替换,发生在预处理阶段,无类型检查,不占存储空间;const 是编译器处理的常量,有类型检查,可以调试。现代C++推荐使用 const / constexpr。
Q2:词法分析器如何区分关键字和标识符?¶
答:词法分析器首先将所有字母开头的连续字符识别为"标识符",然后查关键字表(哈希表或排序数组)判断是否为关键字。有些设计在DFA中直接区分。
Q3:NFA和DFA有什么区别?¶
答:NFA(非确定有限自动机)允许ε转换和多个转换,更易构造但执行慢;DFA(确定有限自动机)每个状态对每个输入只有一个转换,执行快但状态可能更多。通过子集构造法可将NFA转换为DFA。
Q4:词法分析阶段会报哪些错误?¶
答:词法错误包括非法字符(如中文标点)、未闭合的字符串/注释、非法数字格式(如 0x3G)等。大部分结构错误留给语法分析阶段处理。
Q5:为什么编译器要分词法分析和语法分析两个阶段?¶
答:模块化设计的好处:①词法规则用正则表达式描述更简洁高效;②语法分析器不必处理空白和注释;③词法分析可以独立优化(如用DFA实现O(n)扫描);④便于维护和扩展。
📝 本章小结¶
┌─────────────────────────────────────────────┐
│ 本章核心知识点 │
├─────────────────────────────────────────────┤
│ │
│ 1. 预处理:宏展开、文件包含、条件编译 │
│ 2. 词法分析:字符流 → Token流 │
│ 3. Token分类:关键字、标识符、字面量等 │
│ 4. 正则表达式定义Token模式 │
│ 5. DFA/NFA是词法分析的理论基础 │
│ 6. 手写Lexer / 使用Flex生成 │
│ │
└─────────────────────────────────────────────┘
下一章:03-语法分析与AST - 将Token流组织成语法树