Phases of translation
C++源文件通过编译器处理以生成C++程序。
目录 |
翻译过程
C++程序的文本保存在称为 源文件 的单元中。
C++源文件经过 翻译 成为 翻译单元 ,包含以下步骤:
- 将每个源文件映射为字符序列。
- 将每个字符序列转换为由空白符分隔的预处理记号序列。
- 将每个预处理记号转换为记号,形成记号序列。
- 将每个记号序列转换为翻译单元。
C++程序可由经过翻译的翻译单元组成。翻译单元和实例化单元(实例化单元将在下文第8阶段描述)可单独保存或存入库中。多个翻译单元通过(例如)具有外部链接的符号或数据文件进行相互通信。翻译单元可被单独翻译,随后链接以生成可执行程序。
上述过程可以组织为 9 个 翻译阶段 。
预处理记号
一个 预处理记号 (preprocessing token)是在翻译阶段 3 到 6 中语言的最小词法元素。
预处理令牌的类别包括:
- 头文件名 (例如 < iostream > 或 "myfile.h" )
|
(since C++20) |
- 标识符
- 预处理数字(见下文)
- 字符字面量 ,包括 用户定义 字符字面量 (C++11 起)
- 字符串字面量 ,包括 用户定义 字符串字面量 (C++11 起)
- 运算符和标点符 ,包括 替代记号
- 不属于任何其他类别的单个非空白字符
-
如果匹配此分类的字符是以下情况,则程序是非良构的:
- 撇号( ' , U+0027),
- 引号( " , U+0022),或
- 不在 基本字符集 中的字符。
预处理数字
预处理数字的预处理记号集合是 整数字面量 和 浮点数字面量 记号集合的并集的超集:
.
(可选)
digit
pp-continue-seq
(可选)
|
|||||||||
| digit | - | 数字0-9之一 |
| pp-continue-seq | - | pp-continue 的序列 |
每个 pp-continue 是以下之一:
| identifier-continue | (1) | ||||||||
| exp-char sign-char | (2) | ||||||||
.
|
(3) | ||||||||
’
digit
|
(4) | (自 C++14 起) | |||||||
’
nondigit
|
(5) | (自 C++14 起) | |||||||
| identifier-continue | - | 有效 标识符 的任意非首字符 |
| exp-char | - |
以下字符之一
P
,
p
,
(C++11 起)
E
和
e
|
| sign-char | - |
+
和
-
之一
|
| digit | - | 数字 0-9 之一 |
| nondigit | - | 拉丁字母 A/a-Z/z 及下划线之一 |
预处理数字本身没有类型或值;它只有在成功转换为整型/浮点数字面量标记后才同时获得这两者。
空白符
空白 由 注释 、空白字符或两者共同组成。
以下字符为空白字符:
- 字符制表符 (U+0009)
- 换行符 (U+000A)
- 行制表符 (U+000B)
- 换页符 (U+000C)
- 空格符 (U+0020)
空白符通常用于分隔预处理标记,但存在以下例外情况:
-
在头文件名、字符字面量和字符串字面量中,
\不是分隔符。 - 由包含换行符的空白分隔的预处理标记无法构成 预处理指令 。
#include "my header" // 正确:使用包含空格的头部文件名 #include/*hello*/<iostream> // 正确:使用注释作为空白符 #include <iostream> // 错误:#include 指令不能跨越多行 "str ing" // 正确:单个预处理记号(字符串字面量) ' ' // 正确:单个预处理记号(字符字面量)
最大吞噬
最大咬合原则是在阶段3将源文件分解为预处理记号时使用的规则。
如果输入已被解析为预处理记号直至某个给定字符(否则下一个预处理记号不会被解析,这使得解析顺序具有唯一性),通常会将下一个预处理记号视为能构成预处理记号的最长字符序列,即使这会导致后续分析失败。这通常被称为 最大吞噬原则 。
int foo = 1; int bar = 0xE+foo; // 错误:无效的预处理数字 0xE+foo int baz = 0xE + foo; // 正确
换句话说,最大吞噬规则优先支持 多字符运算符和标点符 :
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; // 正确,“int”不是头文件名
- 如果接下来的三个字符是 < :: 且后续字符既不是 : 也不是 > ,则 < 将作为独立的预处理词元处理,而非作为 替代词法符 < : 的首字符。
struct Foo { static const int v = 1; }; std::vector<::Foo> x; // 正确,<: 不会被当作 [ 的替代记号 extern int y<::>; // 正确,等同于 "extern int y[];" int z<:::Foo::value:>; // 正确,等同于 "int z[::Foo::value];"
template<int i> class X { /* ... */ }; template<class T> class Y { /* ... */ }; Y<X<1>> x3; // 正确:声明类型为 “Y<X<1> >” 的变量 “x3” Y<X<6>>1>> x4; // 语法错误 Y<X<(6>>1)>> x5; // 正确
#define R "x" const char* s = R"y"; // 非法的原始字符串字面量,不是 "x" "y" const char* s2 = R"(a)" "b)"; // 原始字符串字面量后接普通字符串字面量 |
(C++11 起) |
词法单元
一个 token 是在翻译阶段7中该语言的最小词法单元。
令牌的类别包括:
翻译阶段
翻译过程 如同 按照从阶段1到阶段9的顺序执行。实现的行为如同这些独立阶段依次发生,尽管在实践中不同阶段可以被合并处理。
阶段 1:映射源字符
|
1)
源文件的各个字节以(由实现定义的方式)映射到
基本源字符集
的字符。特别地,操作系统相关的行结束指示符会被换行符替换。
|
(C++23 前) | ||
|
保证支持作为 UTF-8 代码单元序列的输入文件(UTF-8 文件)。其他支持的输入文件类型集合由实现定义。若该集合非空,则通过包含将输入文件指定为 UTF-8 文件(与内容无关)的实现定义方式确定输入文件类型(识别字节顺序标记不足够)。
|
(C++23 起) |
阶段 2:行拼接
阶段 3:词法分析
// The following #include directive can de decomposed into 5 preprocessing tokens: // punctuators (#, < and >) // │ // ┌────────┼────────┐ // │ │ │ #include <iostream> // │ │ // │ └── header name (iostream) // │ // └─────────── identifier (include)
// Error: partial string literal "abc
// Error: partial comment /* comment
|
当从源文件消耗字符以形成下一个预处理令牌时(即不作为注释或其他形式空白的一部分被消耗),通用字符名会被识别并替换为
翻译字符集
的指定元素,除非匹配以下任一预处理令牌中的字符序列:
|
(since C++23) |
| (自C++11起) |
- 每个注释被替换为一个空格字符。
- 换行字符予以保留。
- 非换行的连续空白字符序列是否保留或被替换为单个空格字符由实现定义。
阶段 4:预处理
阶段5:确定通用字符串字面量编码
|
2)
字符字面量和非原始字符串字面量中的
转义序列
和通用字符名会被展开并转换为字面量编码。
如果通用字符名指定的字符无法在相应的字面量编码中编码为单个码点,则结果由实现定义,但保证不会是空(宽)字符。 |
(C++23 前) |
|
对于两个或更多相邻的 字符串字面量 标记序列,将按照 此处 所述确定一个公共编码前缀。随后每个这样的字符串字面量标记都被视为具有该公共编码前缀。 (字符转换移至阶段3) |
(C++23 起) |
阶段 6:字符串字面量连接
相邻的 字符串字面量 会被连接起来。
阶段 7:编译
编译过程发生:每个预处理记号被转换为一个 记号 。这些记号经过语法和语义分析后,作为 翻译单元 进行翻译。
阶段 8:实例化模板
每个翻译单元都会被检查以生成所需的模板实例化列表,包括由 显式实例化 所请求的实例化。模板的定义被定位,然后执行所需的实例化操作以生成 实例化单元 。
阶段 9:链接
翻译单元、实例化单元以及满足外部引用所需的库组件被收集到程序映像中,该映像包含在其执行环境中执行所需的信息。
注释
源文件、翻译单元和已翻译的翻译单元不必以文件形式存储,这些实体与任何外部表示之间也不一定存在一一对应关系。该描述仅为概念性说明,并未规定任何具体实现方式。
|
阶段5执行的转换可通过某些实现中的命令行选项控制:gcc和clang使用 - finput - charset 指定源字符集的编码,使用 - fexec - charset 和 - fwide - exec - charset 分别指定普通字面值和宽字面值的编码;而Visual Studio 2015 Update 2及更高版本使用 / source - charset 和 / execution - charset 分别指定源字符集和字面值编码。 |
(until 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在拼接后检查文件结束拼接,此操作非必需 | 移除该检查 |
| P2621R3 | C++98 | 不允许通过行拼接或记号连接形成通用字符名称 | 允许该操作 |
参考文献
- C++23 标准 (ISO/IEC 14882:2024):
-
- 5.2 翻译阶段 [lex.phases]
- C++20 标准 (ISO/IEC 14882:2020):
-
- 5.2 翻译阶段 [lex.phases]
- C++17 标准 (ISO/IEC 14882:2017):
-
- 5.2 翻译阶段 [lex.phases]
- C++14 标准 (ISO/IEC 14882:2014):
-
- 2.2 翻译阶段 [lex.phases]
- C++11 标准 (ISO/IEC 14882:2011):
-
- 2.2 翻译阶段 [lex.phases]
- C++03 标准 (ISO/IEC 14882:2003):
-
- 2.1 翻译阶段 [lex.phases]
- C++98 标准 (ISO/IEC 14882:1998):
-
- 2.1 翻译阶段 [lex.phases]
参见
|
C 文档
关于
翻译阶段
|