C++霧中風景番外篇:理解C++的複雜宣告與宣告解析

flynike發表於2021-09-09

在學習C系列語言的過程之中,理解C/C++的複雜宣告一直是初學者很困擾的問題。筆者初學之時也深受困擾,對很多規則死記硬背。後續在閱讀《C專家程式設計》之後,嘗試在編譯器的角度來理解C/C++的宣告解析,並且編寫程式碼將這部分邏輯串聯起來,之後再看到許多看似複雜的宣告,也能夠很好的理解和消化了。

1.複雜的宣告

在編寫C/C++程式碼時偶爾能看到如下的複雜宣告:float(*(*e[10])(int*))[5]。我想你的第一反應一定是:MMP。雖然我們在實際工作之中是很少出現這種極其複雜的宣告邏輯,同時也不提倡使用這樣的宣告。但是學會理解和解析這類複雜的宣告邏輯,可以更好的理解C/C++之中諸個關鍵詞是如何進行組織,來表達邏輯的,也能更好的理解各個關鍵詞的使用方式。

比如之前筆者寫的一篇文章之中整理了C/C++之中const關鍵詞的用法 《C++霧中風景3: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 << this_t.content + " is ";
gettoken();

}

* **按照優先順序法則處理邏輯,先右後左,遇到括號彈出之後繼續上述邏輯**

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 << token_stack.top().content + " ";
        token_stack.pop();
    }
}

}

* **處理陣列型別的函式**

void deal_with_arrays() {
while (this_t.type == LBRACKET) {
cout << “array “;
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 << "function returning ";

}
“`

所以通過上述的程式碼串聯起來,我們就可以簡單的完成一個解析C/C++宣告的小程式。嘗試這個小程式解析筆者在本文提出的示例:
用程式碼進行宣告解析
上述實現程式碼的完整版,筆者放在了自己的github之上,需要的可以自取。《C專家程式設計》之中也有對應C語言版本,需要的也可以用作參考。

4.小結

厭倦了複雜宣告?希望有更友好的宣告型別?番外篇當然是為了引出正篇,接下來筆者將會和大家一起來看看,C++為了簡化宣告的型別系統,做出了那些努力來更加高效的提升程式設計師的工作效率。A

相關文章