命名空间
变体
操作

翻译阶段

来自 cppreference.cn
< cpp‎ | 语言
 
 
C++ 语言
 
 

C++ 源文件由编译器处理以生成 C++ 程序。

目录

[编辑] 翻译过程

C++ 程序的文本保存在称为 源文件 的单元中。

C++ 源文件经过 翻译 成为一个 翻译单元,包括以下步骤

  1. 将每个源文件映射到字符序列。
  2. 将每个字符序列转换为预处理记号序列,由空白分隔。
  3. 将每个预处理记号转换为记号,形成记号序列。
  4. 将每个记号序列转换为翻译单元。

C++ 程序可以由翻译后的翻译单元组成。翻译单元可以单独翻译,然后链接以生成可执行程序。

上述过程可以组织成 9 个翻译阶段

[编辑] 预处理记号

预处理记号 是翻译阶段 3 到 6 中语言的最小词法元素。

预处理记号的类别是

(C++20 起)
如果匹配此类别的字符是以下之一,则程序格式错误
  • 撇号(',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 起) Ee
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 起)
  • import 指令中的 import 预处理记号之后
(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) 源代码文件的单个字节被(以实现定义的方式)映射到基本源字符集中的字符。特别是,与操作系统相关的行尾指示符被换行符替换。
2) 接受的源文件字符集是实现定义的(C++11 起)。任何不能映射到基本源字符集中字符的源文件字符都被其通用字符名称(用 \u\U 转义)或某种等效处理的实现定义形式替换。
3) 三字符序列被替换为相应的单字符表示。
(C++17 前)
(直至 C++23)

保证支持作为 UTF-8 码元序列(UTF-8 文件)的输入文件。其他支持的输入文件种类集是实现定义的。如果该集非空,则输入文件的种类以实现定义的方式确定,包括一种将输入文件指定为 UTF-8 文件的方法,独立于其内容(识别字节顺序标记不足)。

  • 如果输入文件被确定为 UTF-8 文件,则它应是一个格式良好的 UTF-8 码元序列,并被解码以生成 Unicode 标量值序列。然后,通过将每个 Unicode 标量值映射到相应的翻译字符集元素,形成一个翻译字符集元素的序列。在结果序列中,输入序列中由回车符(U+000D)后跟换行符(U+000A)组成的每对字符,以及未紧跟换行符(U+000A)的回车符(U+000D),都被单个换行符替换。
  • 对于实现支持的任何其他类型的输入文件,字符以(实现定义的方式)映射到翻译字符集元素的序列。特别是,与操作系统相关的行尾指示符被换行符替换。
(C++23 起)

[编辑] 阶段 2:拼接行

1) 如果第一个翻译字符是字节顺序标记 (U+FEFF),则将其删除。(C++23 起)当反斜杠(\)出现在行尾时(紧跟零个或更多非换行符的空白字符,然后是(C++23 起)换行符),这些字符被删除,将两个物理源行合并为一个逻辑源行。这是一个单次操作;以两个反斜杠后跟空行结尾的行不会将三行合并为一行。
2) 如果非空源文件在此步骤之后(此时行尾反斜杠不再是拼接符)没有以换行符结尾,则添加一个终止换行符。

[编辑] 阶段 3:词法分析

1) 源文件被分解为预处理记号空白
// 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-char-sequence
  • 字符串字面量(s-char-sequencer-char-sequence),不包括分隔符(d-char-sequence
  • 头文件名称(h-char-sequenceq-char-sequence
(C++23 起)


2) 任何在阶段 1 和(C++23 前)阶段 2 期间在任何原始字符串字面量的初始和最终双引号之间执行的转换都将恢复。
(C++11 起)
3) 空白被转换
  • 每个注释被一个空格字符替换。
  • 换行符被保留。
  • 除换行符以外的每个非空空白字符序列是保留还是替换为一个空格字符是未指定的。

[编辑] 阶段 4:预处理

1) 预处理器执行。
2) 每个使用 #include 指令引入的文件都递归地经过阶段 1 到阶段 4。
3) 在此阶段结束时,所有预处理指令都从源文件中移除。

[编辑] 阶段 5:确定通用字符串字面量编码

1) 字符字面量字符串字面量中的所有字符都从源字符集转换为编码(它可以是多字节字符编码,例如 UTF-8,只要基本字符集的 96 个字符具有单字节表示)。
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 分别指定源字符集和字面量编码。

(直至 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 文档 关于 翻译阶段