Namespaces
Variants

Phases of translation

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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

目录

翻译过程

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

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

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

C++程序可由经过翻译的翻译单元组成。翻译单元和实例化单元(实例化单元将在下文第8阶段描述)可单独保存或存入库中。多个翻译单元通过(例如)具有外部链接的符号或数据文件进行相互通信。翻译单元可被单独翻译,随后链接以生成可执行程序。

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

预处理记号

一个 预处理记号 (preprocessing token)是在翻译阶段 3 到 6 中语言的最小词法元素。

预处理令牌的类别包括:

(since 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 起) 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 起)
  • import 指令中 import 预处理令牌之后
(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) 源文件的各个字节以(由实现定义的方式)映射到 基本源字符集 的字符。特别地,操作系统相关的行结束指示符会被换行符替换。
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),则将其删除。 (since C++23) 当反斜杠( \ )出现在行尾(紧跟着 零个或多个非换行符的空白字符后接 (since 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-sequence r-char-sequence ),不包括定界符( d-char-sequence
  • 头文件名( h-char-sequence q-char-sequence
(since 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 分别指定源字符集和字面值编码。

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