翻译阶段
编译器会处理 C++ 源文件,并产生 C++ 程序。
翻译
C++ 程序文本会保存在被称为源文件 的单元。
C++ 源文件会通过翻译 成为翻译单元,翻译包含以下步骤:
- 将每个源文件映射到一个字符序列。
- 将每个字符序列转换成一个预处理记号序列,以空白分隔。
- 将每个预处理记号转换成一个记号,以组成记号序列。
- 将每个记号序列转换成一个翻译单元。
翻译后的各个翻译单元可以组成 C++ 程序。多个翻译单元可以分开翻译,并且可以在后续链接在一起来产生可执行程序。
以上流程可以组织为 9 个翻译阶段。
预处理记号
预处理记号 是语言在翻译阶段 3 到阶段 6 中的最小词法元素。
预处理记号有以下种类:
- 标头名(例如 <iostream> 或 "myfile.h")
|
(C++20 起) |
- 标识符
- 预处理数字(见下文)
- 字符字面量,包括用户定义的字符字面量 (C++11 起)
- 字符串字面量,包括用户定义的字符串字面量 (C++11 起)
- 运算符和标点,包括代用记号
- 不属于任何其他类别的单独非空白字符
- 如果匹配此类别的字符是以下之一,那么程序非良构:
- 撇号(',U+0027)
- 引号(",U+0022)
- 基本字符集以外的字符
预处理数字
预处理数字的预处理记号集合是整数字面量和浮点字面量的记号集合的超集:
. (可选) 数位 数字后续序列 (可选)
|
|||||||||
数位 | - | 数位 0-9 之一 |
数字后续序列 | - | 包含数字后续 的序列 |
每个数字后续 都是以下之一:
标识后续 | (1) | ||||||||
幂字符 符号字符 | (2) | ||||||||
.
|
(3) | ||||||||
’ 数位
|
(4) | ||||||||
’ 非数位
|
(5) | ||||||||
标识后续 | - | 任意合法标识符的非首字符 |
幂字符 | - | P 、p 、 (C++11 起)E 和 e 之一
|
符号字符 | - | + 和 - 之一
|
数位 | - | 数位 0-9 之一 |
非数位 | - | 拉丁字母 A/a-Z/z 和下划线之一 |
预处理数字没有类型或值;它需要在成功转换倒整数/浮点字面量记号后才会获得这些属性。
空白
空白 由注释、空白字符或两者共同组成。
以下字符是空白字符:
- 横向制表(U+0009)
- 换行(U+000A)
- 纵向制表(U+000B)
- 换页(U+000C)
- 空格(U+0020)
空白通常用来分隔预处理记号,但有以下例外情况:
- 它在标头名、字符字面量和字符串字面量中不是分隔符。
- 以包含换行符的空白分隔的多个预处理记号不能组成预处理指令。
#include "my header" // OK,使用包含空白的标头名 #include/*hello*/<iostream> // OK,使用注释作为空白 #include <iostream> // 错误:#include 不能跨越多行 "str ing" // OK,单个预处理记号(字符串字面量) ' ' // OK,单个预处理记号(字符字面量)
最大吞噬
如果一个给定字符前的输入已被解析为预处理记号,下一个预处理记号通常会由能构成预处理记号的最长字符序列构成,即使这样处理会导致后续分析失败。这常被称为最大吞噬。
int foo = 1; int bar = 0xE+foo; // 错误:非法的预处理数字 0xE+foo int baz = 0xE + foo; // OK
也就是说,最大吞噬规则偏好多字符运算符和标点符号:
int foo = 1; int bar = 2; int num1 = foo+++++bar; // 错误:被视为 “foo++ ++ +baz”,而不是 “foo++ + ++baz” int num2 = -----foo; // 错误:被视为 “-- -- -foo”,而不是 “- -- --foo”
最大吞噬规则有以下例外:
- 只能在以下情况下组成标头名:
- 在 #include 指令的 include 预处理记号后
|
(C++17 起) |
|
(C++20 起) |
std::vector<int> x; // OK,“int” 不是标头名
- 如果接下来的三个字符是 <::且后继字符不是 : 或者 >,那么把 < 自身当做预处理记号,而非代用记号 <: 的首字符。
struct Foo { static const int v = 1; }; std::vector<::Foo> x; // OK,不会将 <: 当作 [ 的代用记号 extern int y<::>; // OK,同 “extern int y[];” int z<:::Foo::value:>; // OK,同 “int z[::Foo::value];”
template<int i> class X { /* ... */ }; template<class T> class Y { /* ... */ }; Y<X<1>> x3; // OK,声明 “Y<X<1> >” 类型变量 “x3” Y<X<6>>1>> x4; // 语法错误 Y<X<(6>>1)>> x5; // OK
#define R "x" const char* s = R"y"; // 非良构的原始字符串字面量,而非 "x" "y" const char* s2 = R"(a)" "b)"; // 原始字符串字面量后随普通字符串字面量 |
(C++11 起) |
记号
记号 是语言在翻译阶段 7 中的最小词法元素。
记号有以下种类:
翻译阶段
翻译如同以从阶段 1 到阶段 9 的顺序进行。实现的行为如同将这些阶段分开进行,但实践中可以将不同的阶段结合在一起。
阶段 1:映射源字符
1) 将源文件的各个单独字节(以具体实现所定义的方式)映射为基本源字符集的字符。特别是,操作系统相关的行尾指示符均被替换为换行字符。
2) 可以接受的源文件字符的集合由实现定义。 (C++11 起)任何无法被映射到基本源字符集中的字符的源文件字符均被替换为它的通用字符名(用
\u 或 \U 转义),或使用某种(由实现定义的)等效处理的方式。
|
(C++23 前) | ||
保证至少支持 UTF-8 代码单元的序列的输入文件(UTF-8 文件)。其他支持的输入文件的种类的集合由实现定义。该集合不为空时,决定文件种类的方式通过由实现定义且以与内容无关的方式决定(包括指定输入文件为 UTF-8 文件,只识别字节序标记无法满足该要求)。
|
(C++23 起) |
阶段 2:拼接行
阶段 3:词法分析
// 以下 #include 指令可以分解成 5 个预处理记号: // 标点符号(#、< 和 >) // │ // ┌────────┼────────┐ // │ │ │ #include <iostream> // │ │ // │ └── 标头名(iostream) // │ // └─────────── 标识符(include)
// 错误:不完整的字面量 "abc
// 错误:不完整的注释 /* 注释
在组成预处理记号而吸收字符时(即不组成注释或其他形式的空白),通用字符名会被识别并被翻译字符集中的指定元素替换,除非正在匹配以下内容中的字符序列:
|
(C++23 起) |
(C++11 起) |
- 以一个空格字符替换每段注释。
- 保留换行符。
- 未指定是否可以将不含换行符的空白缩减成单个空格字符。
阶段 4:预处理
阶段 5:确定字符串字面量的公共编码
(C++23 前) | |
对于每个含有多个相邻字符串字面量记号的序列,都会有一个以此规则指定的共同编码前缀。其中每个字符串字面量记号都会被视为拥有该共同编码前缀。 (字符转换改为在阶段 3 执行) |
(C++23 起) |
阶段 6:拼接字符串字面量
拼接相邻的字符串字面量。
阶段 7:编译
进行编译:将各个预处理记号转换成记号。将所有记号当作一个翻译单元进行语法和语义分析并进行翻译。
阶段 8:实例化模板
检验每个翻译单元,产生所要求的模板实例化的列表,其中包括显式实例化所要求的实例化。定位模板定义,并进行所要求的实例化,以产生实例化单元。
阶段 9:链接
将翻译单元、实例化单元和为满足外部引用所需的库组件汇集成一个程序映像,它含有在它的执行环境中执行所需的信息。
注解
源文件、翻译单元和翻译后的翻译单元不需要存储为未见,这些实体也不需要和它们的外部表示一一对应。这些描述仅存在于概念上,不指定任何特定的实现方式。
某些实现能以命令行选项控制阶段 5 所进行的转换:gcc 和 clang 用 -finput-charset 指定源字符集的编码,用 -fexec-charset 和 -fwide-exec-charset 指定无编码前缀的 (C++11 起)字符串和字符字面量中的执行字符集的编码,而 Visual Studio 2015 Update 2 及之后版本分别用 /source-charset 和 /execution-charset 指定源字符集和执行字符集。 |
(C++23 前) |
某些编译器不实现实例化单元(又称为模板仓库或模板注册表),而是简单地在阶段 7 编译每个模板实例化,将代码存储在它所显式或隐式要求的对象文件中,然后由链接器在阶段 9 将这些编译后的实例化缩减到一个。
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 787 | C++98 | 非空源文件在阶段 2 结束时如果不以换行符结尾,那么行为未定义 | 此时在结尾添加一个换行符 |
CWG 1104 | C++98 | 代用记号 <: 会导致 std::vector<::std::string> 被作为 std::vector[:std::string> 处理 |
添加新的词法分析规则来解决这种问题 |
CWG 1775 | C++11 | 阶段 2 中在原始字符串字面量内组成通用字符名时行为未定义 | 赋予良好定义 |
CWG 2747 | C++98 | 阶段 2 在拼接后还会检查文件尾是否有拼接点,实际不需要该检查 | 移除该检查 |
P2621R2 | C++98 | 不允许通过拼接行或拼接记号来组成通用字符名 | 允许 |
引用
- C++23 标准(ISO/IEC 14882:2024):
- 5.2 Phases of translation [lex.phases]
- C++20 标准(ISO/IEC 14882:2020):
- 5.2 Phases of translation [lex.phases]
- C++17 标准(ISO/IEC 14882:2017):
- 5.2 Phases of translation [lex.phases]
- C++14 标准(ISO/IEC 14882:2014):
- 2.2 Phases of translation [lex.phases]
- C++11 标准(ISO/IEC 14882:2011):
- 2.2 Phases of translation [lex.phases]
- C++03 标准(ISO/IEC 14882:2003):
- 2.1 Phases of translation [lex.phases]
- C++98 标准(ISO/IEC 14882:1998):
- 2.1 Phases of translation [lex.phases]