17.1 翻译阶段 (Phases of translation)
C 语言的编译器处理代码生成程序的过程,可以分为以下 8 个步骤,称为翻译阶段。
1. 八个阶段的语义顺序
八个翻译阶段
- 将源文件映射到源字符集并识别行结束。
- 删除反斜杠加换行形成的续行。
- 把源文件分解为预处理记号与空白序列,并把注释替换为空格。
- 执行预处理指令,展开宏并处理
#include与条件编译。 - 将字符常量与字符串字面量中的转义序列、通用字符名映射到执行字符集。
- 连接相邻字符串字面量。
- 把记号流翻译为翻译单元,完成语法语义分析并生成目标代码。
- 链接各翻译单元与库,解析外部符号,形成最终程序映像。
实现中可以按需组合或者以不同方式进行这些步骤,但行为必须和这些步骤描述的一致。
2. 工程含义
翻译阶段顺序的三个直接推论
宏替换(阶段 4)先于语法语义分析(阶段 7),所以宏是记号级替换;头文件不是独立编译单元,而是被文本插入包含它的源文件;相邻字符串字面量自动拼接(阶段 6)是语言规则。
c
#include <stdio.h>
#define APP_NAME "Mdr"
#define APP_VER " C23"
int main(void) {
puts(APP_NAME APP_VER);
return 0;
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
输出:
bash
Mdr C23
3. 习题
#11706
⚡2⏳2
下面的程序靠三个细节分别检验八个翻译阶段里的三处规则。先不编译,按语言规则写出每一行输出,再编译核对。
c
#include <stdio.h>
#define P_NAME "Mdr"
int main(void) {
int coun\
ter = 2;
puts(P_NAME " " "C23");
printf("%d %d %d\n", counter, (int)sizeof("ab" "cd"), (int)sizeof("ab\
cd"));
puts("one\
line");
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
- 写出三行输出。
counter的声明跨了两行。这属于哪个阶段处理?在反斜杠后面加一个空格再编译一次,GCC 13.2 给什么诊断、程序还能不能编过?按标准,反斜杠后面直接跟换行才是续行,GCC 这里的行为属于什么?- 第二行输出里两个
sizeof都等于 5,但写法的含义不同:一个是相邻字面量拼接,另一个是续行把两段接到同一个字面量里。分别指明它们落在哪个阶段,并说明为什么结果相同。 - 用
gcc -std=c17 -E -P看预处理输出,指出预处理后的puts(P_NAME " " "C23")变成了什么。为什么-E的输出里三个字符串字面量仍然是分开的,程序运行时却像是一个? - 把
#define P_NAME "Mdr"改成#define P_NAME Mdr,重新编译,抄下诊断。这条错误出现在哪个阶段?被当成标识符的Mdr在后续阶段发生了什么?
#11707
⚡3⏳2
下面这段程序有三处与翻译阶段有关的缺陷,编译能通过,运行结果却与注释写的意图不符,或者报了看不懂的警告。
c
#include <stdio.h>
static void dump(const char *s, int n) {
for (int i = 0; i < n; i++) {
printf("%02x ", (unsigned char)s[i]);
}
putchar('\n');
}
int main(void) {
int limit = 3;
int count = 0; // 上限是 3,改大之前先确认缓冲区 \
count = 1;
printf("count=%d limit=%d\n", count, limit);
printf("%d %d\n", (int)sizeof("\x41B"), (int)sizeof("\x41" "B"));
dump("\x1B31m", (int)sizeof("\x1B31m") - 1);
dump("\x1B" "[31m", (int)sizeof("\x1B" "[31m") - 1);
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 编译并运行,记下完整输出与
-Wall -Wextra下的全部警告。 - 第一行输出为什么是
count=0 limit=3,而不是count=1 limit=3?结合阶段 2 与阶段 3 说明注释里的反斜杠起了什么作用。把这条注释改成/* ... */后,输出恢复成什么? sizeof("\x41B")等于多少?sizeof("\x41" "B")又等于多少?十六进制转义在哪个阶段确定边界,为什么它会把后面的B一并吃进转义里?把"\x41B"换成"\101B"(八进制)后再算一次,说明两种转义的区别。- 两次
dump打印的字节数分别是 3 和 5。哪一个才是合法的 ANSI 颜色转义序列ESC [ 3 1 m?另一份的字节序列是什么?\x1B把后面的3吃进转义之后,剩下的字节为什么是31 6d而不是别的?用dump的结果逐字节对照,并解释-Wall给出「hex escape sequence out of range」的原因。 - 给出修正后的完整程序:注释改成块注释,两处转义都写成
1b 5b 33 31 6d这 5 个字节。要求-Wall -Wextra零警告;两种写法都可以,但至少给一种用八进制\033的、一种用十六进制加相邻字面量的。
#11708
⚡4⏳3
下面四个小程序各自卡在不同的翻译阶段。对每一个,先判断它会在哪一个阶段失败(预处理、语法语义分析、还是链接),再实际用 gcc -std=c17 分步验证:-E 只做预处理,-S 做编译,-c 做汇编,直接编译则含链接。
c
/* a.c */
#define X 1
#if X = 1
int main(void) { return 0; }
#endif1
2
3
4
5
2
3
4
5
c
/* b.c */
int main(void) {
int x = ;
return 0;
}1
2
3
4
5
2
3
4
5
c
/* c.c */
int missing(void);
int main(void) { return missing(); }1
2
3
2
3
c
/* d.c 与 d.h */
/* d.h */
int util_add(int a, int b) {
return a + b;
}
/* d.c */
#include "d.h"
#include "d.h"
int main(void) { return util_add(1, 2); }1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 填一张 4 行 4 列的表,行是四个程序,列是
-E、-S、-c、直接编译四步,格子里写成功或失败。 a.c的#if X = 1为什么要用==?预处理器把=当成了什么?这条诊断在-E阶段就出现,说明了什么?b.c的-E成功而-S失败,c.c三步都成功、只有链接失败。用自己的话说明这两处失败分别落在八个阶段中的哪一个,以及为什么预处理阶段看不出这两类问题。d.c的-E成功,但-S失败。为什么重复包含在预处理阶段不报错,却在编译阶段报「重定义」?给出三种修法并说明各自落在哪个阶段:包含保护(#ifndef守护宏)、static或static inline函数、把定义移进单独的.c文件只在头文件留声明。修好之后-E的输出有什么变化?- 把
c.c里的missing改成static,链接错误变成什么?这条新诊断和原诊断分别由谁发出?
#11709
⚡5⏳4
正文说「头文件不是独立编译单元,而是被文本插入包含它的源文件」,又说「宏是记号级替换」。这两句话合起来意味着:一个头文件的内容取决于它被包含那一刻的宏状态,而不同翻译单元可以有不同状态。下面的工程把配置放进了头文件,但每个 .c 都自己定义了 USE_FLOAT_MODE,于是两个翻译单元对 sample_t 的理解不一致。
c
#ifndef CONFIG_H
#define CONFIG_H
#ifdef USE_FLOAT_MODE
typedef double sample_t;
#else
typedef int sample_t;
#endif
sample_t half(sample_t x);
unsigned config_signature(void);
#endif1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
c
#define USE_FLOAT_MODE
#include "config.h"
sample_t half(sample_t x) {
return x / 2;
}
unsigned config_signature(void) {
return (unsigned)sizeof(sample_t);
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
c
#include <stdio.h>
#include "config.h"
unsigned config_signature(void);
int main(void) {
printf("reader=%u writer=%u\n",
(unsigned)sizeof(sample_t), config_signature());
return 0;
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
- 编译运行,记下输出。为什么
reader=4而writer=8?把两个数分别对应到各自翻译单元里的sample_t。 - 用
gcc -E -P分别预处理writer.c和reader.c,把typedef那一行截出来。两个翻译单元的预处理结果有什么不同? - 链接器为什么没有报错?
half与config_signature的符号名在两边完全一致,签名差异到哪一步才消失? - 把这个缺陷变成能被链接器抓住:在
config.h里按配置引入一个名字不同的符号(例如cfg_stamp_float_mode与cfg_stamp_int_mode),让每个翻译单元都引用它。给出完整代码,说明此时错误编译会得到什么诊断。 - 说明这类问题为什么是「预处理器把配置复制到每个翻译单元」的直接后果,并给出工程上避免它的两条规则(一条关于配置宏的定义位置,一条关于构建系统)。