翻译阶段
編譯器會處理 C++ 源文件,並產生 C++ 程序。
翻譯
C++ 程序文本會保存在被稱為源文件 的單元。
C++ 源文件會通過翻譯 成為翻譯單元,翻譯包含以下步驟:
- 將每個源文件映射到一個字符序列。
- 將每個字符序列轉換成一個預處理記號序列,以空白分隔。
- 將每個預處理記號轉換成一個記號,以組成記號序列。
- 將每個記號序列轉換成一個翻譯單元。
翻譯後的各個翻譯單元可以組成 C++ 程序。多個翻譯單元可以分開翻譯,並且可以在後續連結在一起來產生可執行程序。
以上流程可以組織為 9 個翻譯階段。
預處理記號
預處理記號 是語言在翻譯階段 3 到階段 6 中的最小詞法元素。
預處理記號有以下種類:
- 標頭名(例如
<iostream>或"myfile.h")
|
(C++20 起) |
- 標識符
- 預處理數字(見下文)
- 字符字面量,包括用戶定義的字符字面量(C++11 起)
- 字符串字面量,包括用戶定義的字符串字面量(C++11 起)
- 運算符和標點,包括代用記號
- 不屬於任何其他類別的單獨非空白字符
- 如果匹配此類別的字符是以下之一,那麼程序非良構:
- 撇號(
',U+0027�� - 引號(
",U+0022) - 基本字符集以外的字符
- 撇號(
預處理數字
預處理數字的預處理記號集合是整數字面量和浮點數字面量的記號集合的超集:
.(可選) 數位 數字後續序列 (可選)
|
|||||||||
| 數位 | - | 數位 0-9 之一 |
| 數字後續序列 | - | 包含數字後續 的序列 |
每個數字後續 都是以下之一:
| 標識後續 | (1) | ||||||||
| 冪字符 符號字符 | (2) | ||||||||
.
|
(3) | ||||||||
』 數位
|
(4) | (C++14 起) | |||||||
』 非數位
|
(5) | (C++14 起) | |||||||
| 標識後續 | - | 任意合法標識符的非首字符 |
| 冪字符 | - | P、p、(C++11 起)E 和 e 之一
|
| 符號字符 | - | + 和 - 之一
|
| 數位 | - | 數位 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 起) |
|
(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 轉義),或使用某種(由實現定義的)等效處理的方式。
|
(C++23 前) | ||
|
保證至少支持 UTF-8 代碼單元的序列的輸入文件(UTF-8 文件)。其他支持的輸入文件的種類的集合由實現定義。該集合不為空時,決定文件種類的方式通過由實現定義且以與內容無關的方式決定(包括指定輸入文件為 UTF-8 文件,只識別字節序標記無法滿足該要求)。
|
(C++23 起) |
階段 2:拼接行
\)在行尾(其後緊跟零或多個除換行符外的空白符,再緊跟(C++23 起)換行符)出現時,刪除這些字符並將兩個物理源碼行組合成一個邏輯源碼行。如果因為這個階段 在原始字符串字面量以外(C++11 起) 組成了通用字符名 ,那麼行為未定義。這是單趟操作:如果有一行以兩個反斜槓結束且後隨一個空行,這三行不會合為一行。
階段 3:詞法分析
// 以下 #include 指令可以分解成 5 个预处理记号:
// 标点符号(#、< 和 >)
// │
// ┌────────┼────────┐
// │ │ │
#include <iostream>
// │ │
// │ └── 标头名(iostream)
// │
// └─────────── 标识符(include)
// 错误:不完整的字面量
"abc
// 错误:不完整的注释
/* 注释
|
在組成預處理記號而吸收字符時(即不組成注釋或其他形式的空白),通用字符名會被識別並被翻譯字符集中的指定元素替換,除非正在匹配以下內容中的字符序列:
|
(C++23 起) |
| (C++11 起) |
- 以一個空格字符替換每段注釋。
- 保留換行符。
- 未指定是否可以將不含換行符的空白縮減成單個空格字符。
階段 4:預處理
階段 5:確定字符串字面量的公共編碼
| (C++23 前) | |
|
對於每個含有多個相鄰字符串字面量記號的序列,都會有一個以此規則指定的共同編碼前綴。其中每個字符串字面量記號都會被視為擁有該共同編碼前綴。 (字符轉換改為在階段 3 執行) |
(C++23 起) |
階段 6:拼接字符串字面量
拼接相鄰的字符串字面量。
階段 7:編譯
進行編譯:將各個預處理記號轉換成記號。將所有記號當作一個翻譯單元進行語法和語義分析並進行翻譯。
階段 8:實例化模板
檢驗每個翻譯單元,產生所要求的模板實例化的列表,其中包括顯式實例化所要求的實例化。定位模板定義,並進行所要求的實例化,以產生實例化單元。
階段 9:連結
將翻譯單元、實例化單元和為滿足外部引用所需的庫組件匯集成一個程序映像,它含有在它的執行環境中執行所需的信息。
註解
源文件、翻譯單元和翻譯後的翻譯單元不需要存儲為文件,這些實體也不需要和它們的外部表示一一對應。這些描述僅存在於概念上,不指定任何特定的實現方式。
|
某些實現能以命令行選項控制階段 5 所進行的轉換:gcc 和 clang 用 |
(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 文檔
|