翻译阶段
C++ 源文件由编译器处理以生成 C++ 程序。
内容 |
[编辑] 翻译过程
C++ 程序的文本保存在称为源文件的单元中。
C++ 源文件经过翻译成为翻译单元,包括以下步骤
- 将每个源文件映射到字符序列。
- 将每个字符序列转换为预处理记号序列,用空白符分隔。
- 将每个预处理记号转换为记号,形成记号序列。
- 将每个记号序列转换为翻译单元。
C++ 程序可以由翻译后的翻译单元构成。翻译单元可以单独翻译,然后链接以生成可执行程序。
以上过程可以组织成 9 个翻译阶段。
[编辑] 预处理记号
预处理记号是翻译阶段 3 到 6 中语言的最小词法元素。
预处理记号的类别有
- 头文件名称(例如 <iostream> 或 "myfile.h")
|
(自 C++20 起) |
- 标识符
- 预处理数字(见下文)
- 字符字面量,包括 用户定义的 字符字面量(自 C++11 起)
- 字符串字面量,包括 用户定义的 字符串字面量(自 C++11 起)
- 运算符和标点符号,包括 替代记号
- 不属于任何其他类别的单个非空白字符
- 如果匹配此类别的字符是,程序是非良构的
- 撇号 (', U+0027),
- 引号 (", U+0022),或
- 不在基本字符集中的字符。
[编辑] 预处理数字
预处理数字的预处理记号集是 整数字面量 和 浮点字面量 的记号集的并集的超集
. (可选) 数字 pp-continue-seq (可选) |
|||||||||
数字 | - | 数字 0-9 之一 |
pp-continue-seq | - | pp-continue s 的序列 |
每个 pp-continue 是以下之一
identifier-continue | (1) | ||||||||
exp-char sign-char | (2) | ||||||||
.
|
(3) | ||||||||
’ 数字 |
(4) | (自 C++14 起) | |||||||
’ nondigit |
(5) | (自 C++14 起) | |||||||
identifier-continue | - | 有效标识符的任何非首字符 |
exp-char | - | 之一 P 、p 、(自 C++11 起) E 和 e |
sign-char | - | + 和 - 之一 |
数字 | - | 数字 0-9 之一 |
nondigit | - | 拉丁字母 A/a-Z/z 和下划线之一 |
预处理数字没有类型或值;它在成功转换为整数/浮点字面量记号后才获得两者。
[编辑] 空白符
空白符由注释、空白字符或两者组成。
以下字符是空白字符
- 字符制表符 (U+0009)
- 换行 / 新行字符 (U+000A)
- 行制表符 (U+000B)
- 换页符 (U+000C)
- 空格 (U+0020)
空白符通常用于分隔预处理记号,但以下情况除外
- 它不是头文件名称、字符字面量和字符串字面量中的分隔符。
- 被包含换行字符的空白符分隔的预处理记号不能形成预处理指令。
#include "my header" // OK, using a header name containing whitespace #include/*hello*/<iostream> // OK, using a comment as whitespace #include <iostream> // Error: #include cannot span across multiple lines "str ing" // OK, a single preprocessing token (string literal) ' ' // OK, a single preprocessing token (character literal)
[编辑] 最大匹配原则
如果输入已被解析为预处理记号到给定字符,则下一个预处理记号通常被认为是可能构成预处理记号的最长字符序列,即使这会导致后续分析失败。这通常被称为最大匹配。
int foo = 1; int bar = 0xE+foo; // Error: invalid preprocessing number 0xE+foo int baz = 0xE + foo; // OK
换句话说,最大匹配规则有利于多字符运算符和标点符号
int foo = 1; int bar = 2; int num1 = foo+++++bar; // Error: treated as “foo++ ++ +baz”, not “foo++ + ++baz” int num2 = -----foo; // Error: treated as “-- -- -foo”, not “- -- --foo”
最大匹配规则有以下例外
- 头文件名称预处理记号仅在以下情况下形成
- 在 #include 指令中的 include 预处理记号之后
|
(自 C++17 起) |
|
(自 C++20 起) |
std::vector<int> x; // OK, “int” is not a header name
- 如果接下来的三个字符是 <:: 并且后续字符既不是 : 也不是 >,则 < 被视为独立的预处理记号,而不是 替代记号 <: 的第一个字符。
struct Foo { static const int v = 1; }; std::vector<::Foo> x; // OK, <: not taken as the alternative token for [ extern int y<::>; // OK, same as “extern int y[];” int z<:::Foo::value:>; // OK, same as “int z[::Foo::value];”
template<int i> class X { /* ... */ }; template<class T> class Y { /* ... */ }; Y<X<1>> x3; // OK, declares a variable “x3” of type “Y<X<1> >” Y<X<6>>1>> x4; // Syntax error Y<X<(6>>1)>> x5; // OK
#define R "x" const char* s = R"y"; // ill-formed raw string literal, not "x" "y" const char* s2 = R"(a)" "b)"; // a raw string literal followed by a normal string literal |
(自 C++11 起) |
[编辑] 记号
记号是翻译阶段 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
当从源文件消耗字符以形成下一个预处理记号时(即,不作为注释或其他形式的空白符的一部分消耗),通用字符名称被识别并替换为翻译字符集的指定元素,但当匹配以下预处理记号之一中的字符序列时除外
|
(自 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 分别指定普通和宽字面量编码,而 Visual Studio 2015 Update 2 及更高版本使用 /source-charset 和 /execution-charset 分别指定源字符集和字面量编码。 |
(直到 C++23) |
一些编译器不实现实例化单元(也称为模板仓库或模板注册表),而只是在阶段 7 编译每个模板实例化,将代码存储在隐式或显式请求它的目标文件中,然后在阶段 9 中链接器将这些已编译的实例化折叠为一个。
[编辑] 缺陷报告
以下行为更改缺陷报告被追溯应用于以前发布的 C++ 标准。
DR | 应用于 | 已发布行为 | 正确行为 |
---|---|---|---|
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 文档 关于 翻译阶段
|