我在翻阅 C Refernce 的”翻译阶段3”时,注意到”++最大吞噬++”原则:

若已经分析输入为到给定字符为止的预处理记号,则通常将能构成一个预处理记号的最长字符序列处理成下个预处理记号,即这会导致后继分析失败。这常被称为最大吞噬 (maximal munch) 。

“++最大吞噬++”原则指出: 编译器会尽可能把能识别成一个预处理记号的多义记号处理成一个记号
类似正则表达式中的”贪婪匹配”

例: a+++++b 译为 a++ ++ +b 而不是 a++ + ++b


根据定义与官方示例, 做以下实验:

1
2
3
4
5
6
7
8
// main.c

int main(void) {
int a = 0xE+b; //! 错误:整数常量的“+b”后缀无效, 编译无法通过
int b = 0xE + b; // 正常: 编译通过
int c = 0xF+b; // 正常: 编译通过
return 0;
}

第一行: 因为E在数字中可以表示科学计数法(如1E10, 2E+2, 1.5E-3), 所以编译器处理”0xE+b”时会将它们放在一起, 导致编译出错。
第二行: 使用空格可以分解预处理记号。
第三行: “0xF”是单义的, 编译器不会把它和”+b”放在一起。

由此可知, 编译器对符号的处理是以字符为基础单位的, 而不是一个逻辑行。
它无法分辨符号的含义, 它不知道”0xE”是一个整体, 它也不知道”E”后面的”+”不是后缀而是加法运算符, 虽然我们一眼就可以看出, 但编译器做不到。

事实上, 上述程序在编译经历5个阶段:

  1. 源码输入
  2. 词法分析, 产生词素
  3. 语法分析, 产生语法树
  4. 语义分析, 校验语法树, 产生解析树
  5. 中间代码生成

而 C 编译器则由以下几个部分构成:

  1. 预处理器
  2. 编译器
  3. 汇编器
  4. 链接器

若在编译时加上--save-temp, 编译器就会保留期间产生的中间代码
其中.i文件为预处理器运行后生成的文件, .s为编译器, .o为汇编器, .out/.exe为最终产物(链接器)
实验程序的预处理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// main.i

# 0 "main.c"
# 0 "<built-in>"
# 0 "<命令行>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<命令行>" 2
# 1 "main.c"
int main(void) {
int a = 0xE+b;
int b = 0xE + b;
int c = 0xF+b;
return 0;
}

编译器结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// main.s

// 这是结果
.file "main.c"

// 正常长这样
.file "main.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
addl $14, -4(%rbp)
movl -4(%rbp), %eax
addl $15, %eax
movl %eax, -8(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 13.2.1 20230728 (Red Hat 13.2.1-1)"
.section .note.GNU-stack,"",@progbits

可以看到编译器输出异常了, 编译器只是加载了文件, 没有生成代码 ,也就是在编译阶段出错了。
使用--verbose也可以发现问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 忽略gcc参数配置

# 预处理开始
#include "..." 搜索从这里开始:
#include <...> 搜索从这里开始:
/usr/lib/gcc/x86_64-redhat-linux/13/include
/usr/local/include
/usr/include
搜索列表结束。
# 预处理结束

# 编译开始
Compiler executable checksum: 5eaad519de86376ffacf24afdb40da84
# 编译结束, 是的, 它只给你看个hash

# 错误信息
main.c: 在函数‘main’中:
main.c:2:13: 错误:整数常量的“+b”后缀无效
2 | int a = 0xE+b;
| ^~~~~

# 接下来是后续正常的情况

# 忽略gcc汇编参数, 汇编开始
GNU assembler version 2.39 (x86_64-redhat-linux) using BFD version version 2.39-9.fc38
# 汇编结束

# 链接
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/13/:/usr/libexec/gcc/x86_64-redhat-linux/13/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/13/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/13/:/usr/lib/gcc/x86_64-redhat-linux/13/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/13/../../../:/lib/:/usr/lib/

结合翻译阶段来看, 可以发现是第三阶段时将”0xE+b”视为一个整体, 第七阶段编译进行到第三阶段产生语法树时发现不对了, 于是报错。

由此可见, 在运算符两旁加空格不仅是为了美观, 更是为了避免因”最大吞噬”导致的编译错误

所以各位, 代码一定要美观啊! 要不然连编译器都受不了了报错啊!