翻譯階段
C++ 原始檔由編譯器處理以生成 C++ 程式。
目錄 |
[編輯] 翻譯過程
C++ 程式的文字儲存在稱為 原始檔 的單元中。
C++ 原始檔經過 翻譯 成為一個 翻譯單元,包括以下步驟
- 將每個原始檔對映到字元序列。
- 將每個字元序列轉換為預處理記號序列,由空白分隔。
- 將每個預處理記號轉換為記號,形成記號序列。
- 將每個記號序列轉換為翻譯單元。
C++ 程式可以由翻譯後的翻譯單元組成。翻譯單元可以單獨翻譯,然後連結以生成可執行程式。
上述過程可以組織成 9 個翻譯階段。
[編輯] 預處理記號
預處理記號 是翻譯階段 3 到 6 中語言的最小詞法元素。
預處理記號的類別是
- 標頭檔案名稱(例如 <iostream> 或 "myfile.h")
|
(C++20 起) |
- 識別符號
- 預處理數字(見下文)
- 字元字面量,包括使用者定義字元字面量(C++11 起)
- 字串字面量,包括使用者定義字串字面量(C++11 起)
- 運算子和標點符號,包括備用記號
- 不屬於任何其他類別的單個非空白字元
- 如果匹配此類別的字元是以下之一,則程式格式錯誤
- 撇號(',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" // OK, using a header name containing whitespace #include/*hello*/<iostream> // OK, using a comment as whitespace #include <iostream> // Error: #include cannot span across multiple lines "str ing" // OK, a single preprocessing token (string literal) ' ' // OK, a single preprocessing token (character literal)
[編輯] 最大匹配
如果輸入已解析成預處理記號直到給定字元,則下一個預處理記號通常被認為是能夠構成預處理記號的最長字元序列,即使這會導致後續分析失敗。這通常被稱為最大匹配。
int foo = 1; int bar = 0xE+foo; // Error: invalid preprocessing number 0xE+foo int baz = 0xE + foo; // OK
換句話說,最大匹配規則有利於多字元運算子和標點符號
int foo = 1; int bar = 2; int num1 = foo+++++bar; // Error: treated as “foo++ ++ +baz”, not “foo++ + ++baz” int num2 = -----foo; // Error: treated as “-- -- -foo”, not “- -- --foo”
最大匹配規則有以下例外
- 標頭檔案名稱預處理記號僅在以下情況下形成
- 在#include 指令中的 include 預處理記號之後
|
(C++17 起) |
|
(C++20 起) |
std::vector<int> x; // OK, “int” is not a header name
- 如果接下來的三個字元是 <::,並且隨後的字元既不是 : 也不是 >,則 < 本身被視為一個預處理記號,而不是備用記號 <: 的第一個字元。
struct Foo { static const int v = 1; }; std::vector<::Foo> x; // OK, <: not taken as the alternative token for [ extern int y<::>; // OK, same as “extern int y[];” int z<:::Foo::value:>; // OK, same as “int z[::Foo::value];”
template<int i> class X { /* ... */ }; template<class T> class Y { /* ... */ }; Y<X<1>> x3; // OK, declares a variable “x3” of type “Y<X<1> >” Y<X<6>>1>> x4; // Syntax error Y<X<(6>>1)>> x5; // OK
#define R "x" const char* s = R"y"; // ill-formed raw string literal, not "x" "y" const char* s2 = R"(a)" "b)"; // a raw string literal followed by a normal string literal |
(C++11 起) |
[編輯] 記號
記號 是翻譯階段 7 中語言的最小詞法元素。
記號的類別是
[編輯] 翻譯階段
翻譯按從階段 1 到階段 9 的順序彷彿執行。實現的行為彷彿這些獨立的階段發生,儘管在實踐中不同的階段可以合併在一起。
[編輯] 階段 1:對映源字元
1) 原始碼檔案的單個位元組被(以實現定義的方式)對映到基本源字元集中的字元。特別是,與作業系統相關的行尾指示符被換行符替換。
|
(直至 C++23) | ||
保證支援作為 UTF-8 碼元序列(UTF-8 檔案)的輸入檔案。其他支援的輸入檔案種類集是實現定義的。如果該集非空,則輸入檔案的種類以實現定義的方式確定,包括一種將輸入檔案指定為 UTF-8 檔案的方法,獨立於其內容(識別字節順序標記不足)。
|
(C++23 起) |
[編輯] 階段 2:拼接行
[編輯] 階段 3:詞法分析
// 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++23 起) |
(C++11 起) |
- 每個註釋被一個空格字元替換。
- 換行符被保留。
- 除換行符以外的每個非空空白字元序列是保留還是替換為一個空格字元是未指定的。
[編輯] 階段 4:預處理
[編輯] 階段 5:確定通用字串字面量編碼
(直至 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 分別指定源字元集和字面量編碼。 |
(直至 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 文件 關於 翻譯階段
|