C++霧中風景番外篇:理解C++的複雜宣告與宣告解析
在學習C系列語言的過程之中,理解C/C++的複雜宣告一直是初學者很困擾的問題。筆者初學之時也深受困擾,對很多規則死記硬背。後續在閱讀《C專家程式設計》之後,嘗試在編譯器的角度來理解C/C++的宣告解析,並且編寫程式碼將這部分邏輯串聯起來,之後再看到許多看似複雜的宣告,也能夠很好的理解和消化了。
1.複雜的宣告
在編寫C/C++程式碼時偶爾能看到如下的複雜宣告:float(*(*e[10])(int*))[5]
。我想你的第一反應一定是:MMP。雖然我們在實際工作之中是很少出現這種極其複雜的宣告邏輯,同時也不提倡使用這樣的宣告。但是學會理解和解析這類複雜的宣告邏輯,可以更好的理解C/C++之中諸個關鍵詞是如何進行組織,來表達邏輯的,也能更好的理解各個關鍵詞的使用方式。
比如之前筆者寫的一篇文章之中整理了C/C++之中const關鍵詞的用法 的之中透過口訣的方式記憶const關鍵字在宣告之中的先後順序來釐清不同的邏輯。這種方式不僅效率低下,而且並沒有理解到為什麼不同的先後順序會對宣告邏輯產生影響。在本篇文章之中,筆者嘗試帶大家忘記這些口訣,從編譯器的角度去理解編譯器是如何處理這些宣告的邏輯,知其然而知其所以然。
2.優先順序規則
C/C++的宣告模型是及其晦澀的,筆者簡單統計了涉及宣告模型的關鍵字如const,volatile等大概有十個左右。更為複雜的是在C/C++之中這些關鍵字的先後順序與括號可以任意組合並且發生看起來很奇妙的"化學反應"。
萬變而不離其中,總結出規律之後,再複雜的模型也可以簡化成我們可以理解的單元來處理。所以我們先來看看C/C++宣告的優先順序規則。
宣告是由識別符號,也就是它的名字開始解析的。
獲取了宣告之後,接下來安裝如下優先順序別來依次處理宣告:
1. 優先處理括號部分的宣告邏輯。
2. 優先處理字尾運算子,如[],()
3. 處理字首運算子,如*,const後續可以依次從右往左處理之前的宣告瞭。
掌握了上述的優先順序規則之後,我們回到本文一開始舉的一個小栗子
1.找到宣告e,e將作為宣告的名字。 2.處理字尾運算子,也就是e代表的是一個容量為10的陣列。 3.回到字首運算子,該陣列儲存的內容為指標。 4.跳出括號,開始新的一輪的**優先順序規則**,處理字尾運算子(),我們 發現這個指標指向的是一個引數為int*的函式。 5.接著再次回到字首運算子,所以這個函式返回值依然是一個指標。 6.跳出括號,繼續前文的邏輯,我們發現該指標指向了一個內容為float,容量為5的陣列。 透過上述栗子我們不難發現,對於宣告的處理本質上是一個有限自動機的狀態變化過程,所以編譯器同樣也是按照上述的規律來理解並處理程式的複雜宣告的。瞭解了優先順序規則,我們也就不難去實現一個簡單的小程式**cdecl**來處理宣告邏輯了。####3.簡單的程式碼實現透過上述流程的說明,我們很容易想到可以用**棧**來儲存宣告識別符號左邊的內容,而名字右邊的內容則依照優先順序規則依次處理。(優先處理陣列與函式)。* **先分類將要處理宣告的種類,並且宣告token型別來進行處理**
enum type_tag {IDENTIFIER,QUALIFIER,TYPE,POINTER,LPAREN, LBRACKET,RPAREN,RBRACKET};
struct token {
type_tag type;
string content;
};
* **不斷讀取token,並且壓入棧中,直到讀取到宣告識別符號**
void read_to_first_identifer() {
gettoken();
while (this_t.type != IDENTIFIER) {
token_stack.push(this_t);
gettoken();
}
cout}
* **按照優先順序法則處理邏輯,先右後左,遇到括號彈出之後繼續上述邏輯**void deal_with_declarator(){
switch (this_t.type) {
case LBRACKET:deal_with_arrays();break;
case LPAREN:deal_with_function_args();
}deal_with_pointers();while(!token_stack.empty()) { if(token_stack.top().type == LPAREN) { token_stack.pop(); gettoken(); deal_with_declarator(); } else { cout}
* **處理陣列型別的函式**void deal_with_arrays() {
while (this_t.type == LBRACKET) {
cout gettoken();
if(isdigit(this_t.content[0])) {
printf("0....%d of ",atoi(this_t.content.c_str()) - 1);
gettoken();
}gettoken(); }}
* **處理函式型別的函式**void deal_with_function_args() {
while(this_t.type != RPAREN) {
gettoken();
}
gettoken();cout}
```所以透過上述的程式碼串聯起來,我們就可以簡單的完成一個解析C/C++宣告的小程式。嘗試這個小程式解析筆者在本文提出的示例:
上述實現程式碼的,筆者放在了自己的github之上,需要的可以自取。《C專家程式設計》之中也有對應C語言版本,需要的也可以用作參考。4.小結
厭倦了複雜宣告?希望有更友好的宣告型別?番外篇當然是為了引出正篇,接下來筆者將會和大家一起來看看,C++為了簡化宣告的型別系統,做出了那些努力來更加高效的提升程式設計師的工作效率。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2618/viewspace-2814512/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- C++ 宣告與定義C++
- C++霧中風景5:Explicit's better thaC++
- C++霧中風景12:聊聊C++中的Mutex,以及拯救生產力的BoostC++Mutex
- C++ 型別宣告C++型別
- C++霧中風景10:聊聊左值,純右值與將亡值C++
- C++與Rust變數宣告的比較C++Rust變數
- Effective C++ 4.設計與宣告C++
- C++霧中風景18:C++20, 從concept開始C++
- [C++]變數宣告與定義的規則C++變數
- C++霧中風景17:模板的非推斷語境與std::type_identityC++IDE
- C++霧中風景15:聊聊讓人抓狂的Name ManglingC++
- C++ 與複雜性文化C++
- c++中模板_類别範本的宣告和定義C++
- C++ 提示未宣告的識別符號C++符號
- C++ 陣列宣告和初始化C++陣列
- C++霧中風景16:std::make_index_sequence, 來試一試新的黑魔法吧C++Index
- 詳解C/C++函式指標宣告C++函式指標
- 《Effective C++》閱讀總結(四): 設計、宣告與實現C++
- 《Effective C++》第4章 設計與宣告(2)-讀書筆記C++筆記
- 《Effective C++》第4章 設計與宣告(1)-讀書筆記C++筆記
- 白翔:複雜開放場景中的文字理解
- 深入解析JS變數宣告和函式宣告提升JS變數函式
- Typescript複雜型別的宣告:寫一個工具函式庫TypeScript型別函式
- 用C++模板描述的連結串列、棧、佇列(宣告與實現) (轉)C++佇列
- 【C++注意事項】2 變數宣告和定義C++變數
- TypeScript魔法堂:函式型別宣告其實很複雜TypeScript函式型別
- 【C++進階筆記】(1)函式模板的宣告及使用C++筆記函式
- Effective c++(筆記) 之 類與函式的設計宣告中常遇到的問題C++筆記函式
- c++中模板類的成員函式的宣告與定義應該放在標頭檔案裡C++函式
- 《Effective C++》第三版-4. 設計與宣告(Design and Declarations)C++
- C++ 解引用與函式基礎:記憶體地址、呼叫方法及宣告C++函式記憶體
- C++類將函式模板宣告為友元 例項C++函式
- C++函式中那些不可以被宣告為虛擬函式的函式C++函式
- C++雜思錄——風格的選擇 (轉)C++
- 用預編譯去理解函式宣告提升和變數宣告提升編譯函式變數
- HTML不同版本與宣告HTML
- C++中的複製控制C++
- ES6 --- 新的變數宣告方式 let 與 const 解析變數