翻译阶段

出自cppreference.com


 
 
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) (C++14 起)
非數位 (5) (C++14 起)
標識後續 - 任意合法標識符的非首字符
冪字符 - 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++ ++ +bar”,而不是 “foo++ + ++bar”
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 起)換行符)出現時,刪除這些字符並將兩個物理源碼行組合成一個邏輯源碼行。如果因為這個階段 原始字符串字面量以外(C++11 起) 組成了通用字符名 ,那麼行為未定義。這是單趟操作:如果有一行以兩個反斜槓結束且後隨一個空行,這三行不會合為一行。
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 在拼接後還會檢查文件尾是否有拼接點,實際不需要該檢查 移除該檢查
P2621R3 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]

參閱

翻譯階段C 文檔