网站首页 > 技术文章 正文
一句话摘要:先懂“翻译阶段”(translation phases),再谈“词法分析”。很多“玄学报错”,其实都栽在这两关。
一、C 标准给编译“划了 8 步”:记这个口令——映、拼、分、展、转、串、编、链
- 字符映射(basic source character set)
把物理文件编码统一成编译器内部表述;同时处理换行归一化,历史遗留的 trigraph 等。
易错:源文件是 GBK/Big5,工程按 UTF-8 编译;字符串/注释字节“炸成火星文”。 - 行拼接
反斜杠 \ 紧跟换行会被删除,相当于把两行接在一起。
易错:\ 后面有空格/Tab → 不拼接;Windows/Linux 混用换行符也会翻车。 - 切分预处理记号(preprocessing token)
把源流切成预处理记号与空白序列;/*…*/、// 注释被替换为一个空格。
易错:注释不是“消失”,而是留下一个空格,可能改变分隔/拼接效果。 - 预处理
宏展开、条件编译、#include、_Pragma 等都在这一步。
易错:宏没加括号、宏里有副作用、字符串化/连接顺序误解(第 5 章详解)。 - 常量转换
字符/字符串字面量里的转义被解释,内容转到执行字符集(execution character set)。
易错:"\xE4\xB8\xAD" 是否真等于 “中” 取决于执行字符集与运行环境。 - 字符串拼接
相邻字符串字面量自动合并:"ab" "cd" → "abcd"。
易错:宏展开后意外变成相邻字符串,从而被自动拼接。 - 编译(语法/语义分析与生成目标文件)
这时输入不再是“预处理记号”,而是记号(token)。 - 链接
把多个翻译单元与库合并为可执行文件/目标库。
二、从文本到“记号”:编译器眼里的代码长这样
示例:
int main() {
int x = 42;
return x + 1;
}
词法分析切分后的**记号(token)**序列大致为:
[int] [main] [(] [)] [{]
[int] [x] [=] [42] [;]
[return] [x] [+] [1] [;]
[}]
注意:42、1 是整数常量记号,与字符 '4'、'2' 无关。
三、两层“词法”的边界:预处理记号vs.记号
- 预处理记号(preprocessing token):服务于预处理器。例如 #include 行里的 <stdio.h> 会被识别为一个整体的头名字(header name)。
- 记号(token):进入语法分析器的输入。关键字 int、运算符 +=、标识符 foo、常量 123 等。
典型误区
脱离 #include,x = <stdio.h>; 会被切成 <、stdio、.、h、>,不是“头名字”。#if 1e3 中的 1e3 首先是预处理数(pp-number),再在预处理常量表达式里求值。
四、最长匹配原则(maximal munch):20 个高频“玄学”其实很朴素
- 运算符优先取最长:>>= 是一个记号,不是 >> 与 =。
- x-->y 切分为:x、--、>、y(没有 --> 这种运算符)。
- a+++b 切分为:a++ 与 +b,不是 a + ++b。
- intmain 是一个标识符,不是 int + main。
- typedef 声明出的“类型名”在词法层仍是标识符记号,是否当类型由语义分析决定。
- 1..2 会被切成 1. 与 .2;不是“1 到 2”。
- 十六进制浮点合法:0x1.fp3、0x1p-1 都是标准写法。
- 相邻字符串必拼接:"ab""cd" → "abcd";注释在第 3 阶段被替换为一个空格,"ab"/*x*/"cd" 仍会拼接。
- 八进制陷阱:012 是八进制常量,值 10(十进制);08 非法。
- 字符常量类型是 int:'a' 不是 char。
- 多字符字符常量 'ab' 合法但其值实现定义,跨平台不可靠。
- == 与 = 是不同记号;if (a = b) 语义错误但词法/语法都能过。
- 行末 \ 与 // 注释互相影响:// 会吞到行末,可能让[2]行拼接失效。
- 宏副作用与求值顺序:INC() + INC() 极易“踩雷”。
- header name 只存在于 #include 上下文;其他上下文不识别。
- pp-number 与表达式求值是两回事:#if 08 在不同实现下会报错或产生非预期。
- digraph/trigraph 历史遗留,现代项目建议禁用。
- 执行字符集 ≠ 源文件编码;I/O 管线不一致就会乱码。
- 十进制整数字面量倾向推导为有符号类型;八/十六进制更容易落到无符号(见下一节)。
- 宏字符串化(#)与字符串拼接([6]阶段)发生在不同阶段,效果别混淆。
五、字面量深水区:整数/浮点/字符/字符串一次讲透
5.1 整数常量:基数、后缀与类型选择
- 基数:十进制(无前缀)、八进制(前导 0)、十六进制(0x/0X)。
- 后缀:U/u(无符号)、L/l(长整型)、LL/ll(长长整型),顺序大小写均可(如 10uL 合法)。
- 类型推导(要点):
- 十进制:优先选择能容纳该值的有符号类型序列。
- 八/十六进制:更容易被推为无符号类型。
- 示例:
- unsigned int a = 4000000000u; // OK int b = 4000000000; // 可能溢出(实现定义/未定义行为)
5.2 浮点常量:十进制与十六进制浮点
- 十进制:1., .5, 1e-3, 3.14F
- 十六进制浮点:0x1.fp3、0x1p-1 —— 更适合表达 2 的幂及精确位形。
5.3 字符/字符串与字符集
- 常见转义:\n \t \\ \' \" \xAB \123(\x 十六进制,\nnn 八进制)。
- 执行字符集决定程序内存中的实际字节;与源文件编码不是一回事。
- 宽字符/宽字符串:L'中'、L"中文";C11/C17 还支持 u"…"(UTF-16)、U"…"(UTF-32)。
- 通用字符名(UCN):\u4E2D、\U00004E2D 可用于字面量,也可(受规则限制)用于标识符。
六、宏与词法的“三把刀”:括号、字符串化、连接
- 括号护体(防优先级翻车)
#define SQR(x) x * x
int r = SQR(1+2); // 实际:1+2*1+2 = 5
// 正解:
#define SQR(x) ((x) * (x))
- 字符串化(#)与连接(##)
#define SHOW(x) printf(#x " = %d\n", (x))
#define CAT(a, b) a##b
int xy = 42;
SHOW(1+2); // 打印文本为“1+2 = 3”
printf("%d\n", CAT(x,y)); // → 标识符 xy → 42
提醒:字符串化发生在预处理阶段;而相邻字符串的自动拼接发生在第 6 阶段,二者时序不同。
- 副作用地雷
#define INC() (counter++)
int v = INC() + INC(); // 自增两次,且求值顺序不受保障
建议:把复杂宏改成 static inline 内联函数,消除副作用与多次求值问题。
七、字符集与标识符:源字符集、执行字符集、UCN一条链打通
- 源字符集:读取文件时的那一套(受物理编码与编译选项影响)。
- 执行字符集:程序中字符串/字符常量的实际表示。
- UCN 能让你在标识符里写非 ASCII 字符(不建议随意用在公共 API 名上,影响可读性/可移植性)。
- 宽字符/本地化 I/O 要成套使用(wprintf、区域设置、终端/文件编码),链路任一端不一致就会乱码。
八、调试词法/预处理问题的“工具流”
- 看预处理结果:gcc -E / clang -E
- 检查宏展开、条件编译、#include 是否符合预期;观察是否生成了相邻字符串。
- 导出记号(dump tokens):clang -Xclang -dump-tokens
- 把你“以为的切分”和“编译器的切分”并排对比。
- 编码钉死:
- GCC/Clang:-finput-charset=utf-8,-fexec-charset=utf-8
- MSVC:/utf-8(并在工程属性固定执行字符集)
- 警告拉满:-Wall -Wextra -Wconversion 等,把词法级风险尽早变成可见告警。
- 显示不可见字符:在编辑器里打开“显示空白/CRLF”,排查行末 \ 与混合换行。
九、常见问题(FAQ)
- trigraph/digraph 还要管吗?
现代项目建议全部禁用;遇到老代码库需确认编译开关。 - 字符串前缀有哪些?
- C89/C90:L
- C99/C11/C17:L、u(UTF-16)、U(UTF-32)
- UTF-8 前缀 u8 已纳入新标准(请确保编译器/标准开关匹配)。
- 单行注释 //
C99 起标准化;若在 C89 模式编译,可能不被支持。
十、实战快测(含要点)
1) 词法切分写出关键结果:
a+++b // → a++ 与 +b
x-->y // → x, --, >, y
1..2 // → 1. 与 .2
0x1.fp3 + 1e-3f // 十六进制浮点 + 十进制浮点
2) 宏优先级:
#define SQR(x) x * x
int r = SQR(1+2); // 结果? → 5(应加括号)
3) 字符串拼接:
"ab" /* c */ "d" // → "abcd"
"ab" "cd" // → "abcd"
"ab" // x
"cd" // // 换行后不相邻 → 不拼接
4) 预处理表达式:
#if 08 → 08 是 pp-number,后续解析会因“八进制不允许 8”而报错/不符合预期,需避免。
5) #include 语境差异:
#include <stdio.h> 中 <stdio.h> 是头名字(header name);
x = <stdio.h>; 按普通记号切分并报语法错。
结语:三件事,把词法坑清干净
- 总看一次预处理输出(第 4 阶段结果)。
- 必要时 dump 记号,直观看“编译器眼里的切分”。
- 统一编码 + 宏全括号 + 零副作用,把风险消灭在写代码那一刻。
猜你喜欢
- 2025-09-21 C#串口组件的使用方法_c#串口控件使用方法
- 2025-09-21 Python 变量学习_python怎么用变量
- 2025-09-21 生产环境使用HBase,你必须知道的最佳实践 | 百万人学AI
- 2025-09-21 5年程序员总结—这几个C语言问题超纲了,小白勿进
- 2025-09-21 C++编程语言日常20个高级经典用法(含完整源码与输出)
- 2025-09-21 【C语言·023】变长数组的栈分配机制与使用限制
- 2025-09-21 【C语言·006】字符常量与转义序列的编码对应关系
- 2025-09-21 剑指Offer:字符串转整数的实现与边界处理
- 2024-08-06 【PythonTip题库精编300题】第35题:十六进制转换为二进制
- 2024-08-06 学习编程第135天 python编程二、八、十、十六进制转换
- 最近发表
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- mysqlany_value (79)
- static函数和普通函数 (84)
- el-date-picker开始日期早于结束日期 (76)
- js判断是否是json字符串 (75)
- c语言min函数头文件 (77)
- asynccallback (87)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 无效的列索引 (74)