命名空间
变体
操作

翻译阶段

来自 cppreference.cn
< cpp‎ | language
 
 
C++ 语言
通用主题
流程控制
条件执行语句
if
迭代语句 (循环)
for
范围 for (C++11)
跳转语句
函数
函数声明
Lambda 函数表达式
inline 说明符
动态异常规范 (在 C++17* 中弃用)
noexcept 说明符 (C++11)
异常
命名空间
类型
说明符
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
存储期说明符
初始化
 
 

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

内容

[编辑] 翻译过程

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

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

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

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

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

[编辑] 预处理记号

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

预处理记号的类别有

(自 C++20 起)
如果匹配此类别的字符是,程序是非良构的
  • 撇号 (', 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 - 之一 Pp(自 C++11 起) Ee
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 起)
  • 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++ 标准。

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