翻译阶段

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 

编译器会处理 C++ 源文件,并产生 C++ 程序。

翻译

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

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

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

翻译后的各个翻译单元可以组成 C++ 程序。多个翻译单元可以分开翻译,并且可以在后续链接在一起来产生可执行程序。

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

预处理记号

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

预处理记号有以下种类:

  • 标头名(例如 <iostream>"myfile.h"
(C++20 起)
如果匹配此类别的字符是以下之一,那么程序非良构:
  • 撇号(',U+0027)
  • 引号(",U+0022)
  • 基本字符集以外的字符

预处理数字

预处理数字的预处理记号集合是整数字面量浮点字面量的记号集合的超集:

.(可选) 数位 数字后续序列 (可选)
数位 - 数位 0-9 之一
数字后续序列 - 包含数字后续 的序列

每个数字后续 都是以下之一:

标识后续 (1)
幂字符 符号字符 (2)
. (3)
数位 (4)
非数位 (5)
标识后续 - 任意合法标识符的非首字符
幂字符 - Pp (C++11 起)Ee 之一
符号字符 - +- 之一
数位 - 数位 0-9 之一
非数位 - 拉丁字母 A/a-Z/z 和下划线之一

预处理数字没有类型或值;它需要在成功转换倒整数/浮点字面量记号后才会获得这些属性。

空白

空白 由注释、空白字符或两者共同组成。

以下字符是空白字符:

  • 横向制表(U+0009)
  • 换行(U+000A)
  • 纵向制表(U+000B)
  • 换页(U+000C)
  • 空格(U+0020)

空白通常用来分隔预处理记号,但有以下例外情况:

  • 它在标头名、字符字面量和字符串字面量中不是分隔符。
  • 以包含换行符的空白分隔的多个预处理记号不能组成预处理指令
#include "my header"         // OK,使用包含空白的标头名
 
#include/*hello*/<iostream>  // OK,使用注释作为空白
 
#include
<iostream> // 错误:#include 不能跨越多行
 
"str ing"  // OK,单个预处理记号(字符串字面量)
' '        // OK,单个预处理记号(字符字面量)

最大吞噬

如果一个给定字符前的输入已被解析为预处理记号,下一个预处理记号通常会由能构成预处理记号的最长字符序列构成,即使这样处理会导致后续分析失败。这常被称为最大吞噬

int foo = 1;
int bar = 0xE+foo;   // 错误:非法的预处理数字 0xE+foo
int baz = 0xE + foo; // OK

也就是说,最大吞噬规则偏好多字符运算符和标点符号

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; // OK,“int” 不是标头名
  • 如果接下来的三个字符是 <::且后继字符不是 : 或者 >,那么把 < 自身当做预处理记号,而非代用记号 <: 的首字符。
struct Foo { static const int v = 1; };
std::vector<::Foo> x;  // OK,不会将 <: 当作 [ 的代用记号
extern int y<::>;      // OK,同 “extern int y[];”
int z<:::Foo::value:>; // OK,同 “int z[::Foo::value];”
  • 如果接下来的两个字符是 >>,并且其中一个 > 可以完成模板标识,那么该字符会单独视为一个预处理记号,而不是预处理记号 >> 的一部分。
template<int i> class X { /* ... */ };
template<class T> class Y { /* ... */ };
 
Y<X<1>> x3;      // OK,声明 “Y<X<1> >” 类型变量 “x3”
Y<X<6>>1>> x4;   // 语法错误
Y<X<(6>>1)>> x5; // OK
  • 如果以下一个字符开头的字符序列可作为原始字符串字面量的前缀和起始双引号,那么下个预处理记号应当为原始字符串字面量。该字面量由匹配原始字符串模式的最短字符序列组成。
#define R "x"
const char* s = R"y";         // 非良构的原始字符串字面量,而非 "x" "y"
const char* s2 = R"(a)" "b)"; // 原始字符串字面量后随普通字符串字面量
(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) 将源文件分解为预处理记号和空白:
// 以下 #include 指令可以分解成 5 个预处理记号:
 
//       标点符号(#、< 和 >)
//          │
// ┌────────┼────────┐
// │        │        │
   #include <iostream>
//     │        │
//     │        └── 标头名(iostream)
//     │
//     └─────────── 标识符(include)
如果源文件以不完整的预处理记号或不完整的注释结束,那么程序非良构:
// 错误:不完整的字面量
"abc
// 错误:不完整的注释
/* 注释
在组成预处理记号而吸收字符时(即不组成注释或其他形式的空白),通用字符名会被识别并被翻译字符集中的指定元素替换,除非正在匹配以下内容中的字符序列:
  • 字符字面量(c字符序列
  • 字符串字面量(s字符序列r字符序列),但不包括分隔符(d字符序列
  • 标头名(h字符序列q字符序列
(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 指定无编码前缀的 (C++11 起)字符串和字符字面量中的执行字符集的编码,而 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 在拼接后还会检查文件尾是否有拼接点,实际不需要该检查 移除该检查
P2621R2 C++98 不允许通过拼接行或拼接记号来组成通用字符名 允许

引用

  • C++23 标准(ISO/IEC 14882:2024):
  • 5.2 Phases of translation [lex.phases]
  • C++20 标准(ISO/IEC 14882:2020):
  • 5.2 Phases of translation [lex.phases]
  • C++17 标准(ISO/IEC 14882:2017):
  • 5.2 Phases of translation [lex.phases]
  • C++14 标准(ISO/IEC 14882:2014):
  • 2.2 Phases of translation [lex.phases]
  • C++11 标准(ISO/IEC 14882:2011):
  • 2.2 Phases of translation [lex.phases]
  • C++03 标准(ISO/IEC 14882:2003):
  • 2.1 Phases of translation [lex.phases]
  • C++98 标准(ISO/IEC 14882:1998):
  • 2.1 Phases of translation [lex.phases]

参阅