C++的九个翻译阶段

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

翻译过程

C++ 程序的文本保存在称为 源文件 的单元中。
C++ 源文件经过 翻译 成为一个 翻译单元,包括以下步骤:

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

C++ 程序可以由翻译后的翻译单元组成。翻译单元可以单独翻译,然后链接以生成可执行程序。
上述过程可被组织成9个阶段。

阶段一:映射源字符

  1. 源代码文件的单个字节被(以实现定义的方式)映射到基本源字符集中的字符。特别是,与操作系统相关的行尾指示符被换行符替换。
  2. 接受的源文件字符集是实现定义的(C++11 起)。任何不能映射到基本源字符集中字符的源文件字符都被其通用字符名称(用 \u 或 \U 转义)或某种等效处理的实现定义形式替换。
  3. 三字符序列被替换为相应的单字符表示。

阶段二:拼接行

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

阶段三:语法分析

  1. 源文件被分解为预处理记号和空白
// 下面的 #include 指令可以分解为 5 个预处理标记:
 
//     标点符号(#、< 和 >)
//          │
// ┌────────┼────────┐
// │        │        │
   #include <iostream>
//     │        │
//     │        └── 头文件名称 (iostream)
//     │
//     └─────────── 标识符 (include)

如果源文件以部分预处理记号或部分注释结尾,则程序格式错误

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

阶段四:预处理

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

阶段五:确定通用字符串字面量编码

  1. 字符字面量和字符串字面量中的所有字符都从源字符集转换为编码(它可以是多字节字符编码,例如 UTF-8,只要基本字符集的 96 个字符具有单字节表示)。
  2. 字符字面量和非原始字符串字面量中的转义序列和通用字符名称被扩展并转换为字面量编码。
    如果通用字符名称指定的字符不能编码为相应字面量编码中的单个码点,则结果是实现定义的,但保证不是空(宽)字符。

对于两个或更多相邻的字符串字面量记号序列,如此处所述确定公共编码前缀。然后,每个此类字符串字面量记号都被视为具有该公共编码前缀。(字符转换移至阶段 3)

阶段六:连结字符串字面量

相邻的字符串字面量被连接。

阶段七:编译

进行编译:每个预处理记号被转换为一个记号。这些记号经过语法和语义分析,并作为翻译单元进行翻译。

阶段八:实例化模板

检查每个翻译单元以生成所需模板实例化的列表,包括由显式实例化请求的那些。定位模板定义,并执行所需的实例化以生成实例化单元。

阶段九:链接

为满足外部引用所需的翻译单元、实例化单元和库组件被收集到一个程序映像中,该映像包含在执行环境中执行所需的信息。

预处理记号

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

  • 头文件名称(例如 / 或 "myfile.h")
  • 由预处理 import 和 module 指令生成的占位符记号(即 import XXX; 和 module XXX;)
  • 标识符
  • 预处理数字
  • 字符字面量,包括用户定义字符字面量
  • 字符串字面量,包括用户定义字符串字面量
  • 运算符和标点符号,包括备用记号
  • 不属于任何其他类别的单个非空白字符
    如果匹配此类别的字符是以下之一,则程序格式错误
    • 撇号(',U+0027),
    • 引号(",U+0022),或
    • 不在基本字符集中的字符。

预处理数字

预处理数字的预处理记号集是整型字面量和浮点型字面量的记号集的并集的超集
预处理数字没有类型或值;它在成功转换为整型/浮点型字面量记号后获得二者。

空白

空白 由注释、空白字符或两者组成。
以下字符是空白字符

  • 字符制表符 (U+0009)
  • 换行符 / 新行字符 (U+000A)
  • 行制表符 (U+000B)
  • 换页符 (U+000C)
  • 空格 (U+0020)

空白通常用于分隔预处理记号,但以下情况除外

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

最大匹配

如果输入已解析成预处理记号直到给定字符,则下一个预处理记号通常被认为是能够构成预处理记号的最长字符序列,即使这会导致后续分析失败。这通常被称为最大匹配。

int foo = 1;
int bar = 0xE+foo;   // 错误:无效的预处理数字 0xE+foo
int baz = 0xE + foo; // 可以

换句话说,最大匹配规则有利于多字符运算符和标点符号

int foo = 1;
int bar = 2;

// 错误行1:foo+++++bar
int num1 = foo+++++bar; 
// 编译器解析逻辑(最大匹配):
// 第一步:foo++ → 合法的后置自增标记
// 第二步:++ → 又一个后置自增标记(但foo++的结果是右值,不能再自增)
// 第三步:+bar → 加法运算符+bar
// 实际解析:(foo++)++ + bar → 语法错误(右值无法自增)
// 你预期的解析:foo++ + ++bar → 但编译器不会这样拆分

// 错误行2:-----foo
int num2 = -----foo;
// 编译器解析逻辑(最大匹配):
// 第一步:-- → 前置自减标记
// 第二步:-- → 又一个前置自减标记
// 第三步:-foo → 负号运算符-foo
// 实际解析:-- -- -foo → 语法错误(连续的--缺少操作数分隔)
// 你预期的解析:- -- --foo → 同样不符合编译器拆分规则

最大匹配规则有以下例外

  • 头文件名称预处理记号仅在以下情况下形成
    • 在#include 指令中的 include 预处理记号之后
    • 在 __has_include 表达式中
    • 在import 指令中的 import 预处理记号之后
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)"; // 一个原始字符串字面量后紧跟一个普通字符串字面量

记号

记号 是翻译阶段 7 中语言的最小词法元素。
记号的类别是:

  • 标识符
  • 关键字
  • 字面量
  • 运算符和标点符号(预处理运算符除外)

注意

源文件、翻译单元和翻译后的翻译单元不一定以文件形式存储,这些实体与任何外部表示之间也不存在一对一的对应关系。此描述仅是概念性的,不指定任何特定的实现。

posted @ 2026-02-11 21:23  灵垚克府  阅读(0)  评论(0)    收藏  举报