翻译阶段
C++ 源文件由编译器处理以生成 C++ 程序。
目录 |
[编辑] 翻译过程
C++ 程序的文本保存在称为 源文件 的单元中。
C++ 源文件经过 翻译 成为一个 翻译单元,包括以下步骤
- 将每个源文件映射到字符序列。
- 将每个字符序列转换为预处理记号序列,由空白分隔。
- 将每个预处理记号转换为记号,形成记号序列。
- 将每个记号序列转换为翻译单元。
C++ 程序可以由翻译后的翻译单元组成。翻译单元可以单独翻译,然后链接以生成可执行程序。
上述过程可以组织成 9 个翻译阶段。
[编辑] 预处理记号
预处理记号 是翻译阶段 3 到 6 中语言的最小词法元素。
预处理记号的类别是
- 头文件名称(例如 <iostream> 或 "myfile.h")
|
(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" // 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++ 标准。
缺陷报告 | 应用于 | 发布时的行为 | 正确的行为 |
---|---|---|---|
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 文档 关于 翻译阶段
|