1. 程式人生 > >[C語言]聲明解析器cdecl

[C語言]聲明解析器cdecl

因此 font 解析結果 全局 有時 執行 個數 isa clas

一、寫在前面

K&R曾經在書中承認,"C語言聲明的語法有時會帶來嚴重的問題。"。由於歷史原因(BCPL語言只有唯一一個類型——二進制字),C語言聲明的語法在各種合理的組合下會變得晦澀難懂。不過在15級的優先級規則加持下,C語言的聲明仍然有跡可循。這篇文章講解了一個通常取名為"cdecl"(不同於函數調用約定)的小型程序,該程序常用來解析C語言的聲明。本程序的基始版本來源於《C專家編程》p75,約140行代碼。

博主在這個程序的基礎上,增加了兩個模塊的功能:

1、struct/enum/union關鍵字後標簽變量名的甄別

有如下聲明:struct student a;

在這個聲明中student是作為struct後可選的"結構標簽"出現的,a才是變量名稱。

技術分享圖片

2、函數參數的處理

源程序略過了函數參數處理的模塊,在此,我們加入了此功能,盡管有些簡化。

技術分享圖片

二、聲明的組成部分

通常情況下來講,一個C語言聲明由三部分組成:類型說明符+聲明名稱(declarator)+分號,如int a;

技術分享圖片

三、優先級規則

1、聲明從它的名字開始讀取,隨後按照優先級順序依次讀取。

2、優先級從高到低依次是:

2.1、聲明中被括號括起來的那部分

2,2、後綴操作符:

符號 () 表示這是一個函數

符號 [] 表示這是一個數組

2.3、前綴操作符:*代表"指向...的指針"

3、如果const/volatile關鍵字後面緊跟類型說明符(如int),那麽該關鍵字作用於類型說明符。在其他情況下,const/volatile關鍵字作用於它左邊緊鄰的指針星號。

因此運用該規則分析如下聲明:char *(*c[10])();

第一步:找到變量名c

第二步:處理c後的[10],表示"c是一個有10個元素的數組"

第三步:處理c前的*,表示"數組元素為指針"

第四步:處理c所在括號後的括號,表示"數組的元素類型是函數指針"

第五步:處理(*c[10])前的星號,表示"數組元素指向的函數的返回值是一個指針"

第六步:處理char,表示"數組元素指向的函數的返回值是一個指向char的指針"

綜上,該聲明表示:C是一個有10個元素的數組,數組元素類型是函數指針,其所指向的函數的返回值是一個指向char的指針。

四、程序執行流程

由於C語言聲明並不可以從左往右直接解析,所以我們需要一個棧結構來保存在讀取到聲明名稱前的所有字段,以便在讀取到id後再分析。

  1. struct token{
  2. char type;
  3. char string[MAXTOKENLEN];
  4. };
  5. struct token stack[MAXTOKENS];

    將所有字段分為三類:名稱、類型以及限定詞,使用枚舉類型,使之與char type對應。

  6. enum type_tag {
  7. IDENTIFIER,QUALIFIER,TYPE
  8. };

    主函數有兩大功能,一是找到identifier,二是處理剩下的聲明。

  9. int main (void)
  10. {
  11. read_to_first_identifier();
  12. deal_with_declarator();
  13. return 0;
  14. }

    第一個函數從左往右讀入輸入數據,一個讀取一個字段(聲明的基本單位),若字段不是id(標識符),則將其壓入棧中,再讀取下一個字段,直到讀取到字段,該階段任務結束。

    第二個函數在得到id後開始工作。根據語法規則,先讀取id後的字符,判斷其為數組還是函數。在處理完id後的字段後,再依次出棧解析前面的聲明。

技術分享圖片

五、各模塊代碼

5.1、讀取標識符:read_to_first_identifier();

使用一個循環,每次讀取一個字段,並判斷其是否為標識符,是,則退出,並輸出。對於正在讀取的標識符,使用一個全局變量struct token thistoken存儲,在處理完該字段後,若其不為標識符,則壓入棧中。

  1. void read_to_first_identifier()
  2. {
  3. gettoken();
  4. while(thistoken.type != IDENTIFIER)
  5. {
  6. push(thistoken);
  7. gettoken();
  8. }
  9. printf("%s is ",thistoken.string);
  10. gettoken();
  11. }

5.2、讀取各字段:gettoken();

我們假設各個含有英文字母的字段(如類型說明符、標識符等)都以空格隔開,因此我們可以從我們讀取到的第一個非空字符開始,判斷它的類型。標識符前的符號有一下幾種:說明符、指針(*)。所以我們將其單獨處理。

  1. void gettoken()
  2. {
  3. char *p = thistoken.string;
  4. while((*p = getchar()) == ‘ ‘);
  5. if(isalnum(*p))
  6. {
  7. while(isalnum(*++p = getchar()));
  8. ungetc(*p,stdin);
  9. *p = ‘\0‘;
  10. thistoken.type = classify_string();
  11. return ;
  12. }
  13. if(*p == ‘*‘)
  14. {
  15. strcpy(thistoken.string,"pointer to");
  16. thistoken.type = ‘*‘;
  17. return ;
  18. }
  19. thistoken.string[1] = ‘\0‘;
  20. thistoken.type = *p;
  21. return ;
  22. }

對於標識符及聲明符,我們在讀取完一個字段後就判斷其類型。對於‘*‘或其他符號(‘(‘‘[‘等),則直接用符號本身作為其類型。

5.3、解析字段類型:classify_string ();

在我們提取到完整的英文/數字字段後,通過該函數來推斷其類型。通過strcmp()函數,將其與各個類型說明符對比,如果一樣,則返回類型說明符,如type/qualifier。與因為用strcmp()函數來比較字符串時,字符串相等,函數返回值為0。為了在相等時得到我們想要的真值,就需要對其進行取反。除了用"!"外,用宏來解決更方便。

  1. #define STRCMP(a,R,b) (strcmp(a,b) R 0)

因此,字符串的比較就成了如下形式:

  1. if(STRCMP(s,==,"void"))
  2. return TYPE;

如果讀取到的字段並非限定符或者說明符,則認為其為標識符。

  1. enum type_tag classify_string()
  2. {
  3. char *s = thistoken.string;
  4. if(STRCMP(s,==,"const"))
  5. {
  6. strcpy(s,"read-only");
  7. return QUALIFIER;
  8. }
  9. if(STRCMP(s,==,"volatile"))
  10. return QUALIFIER;
  11. if(STRCMP(s,==,"void"))
  12. return TYPE;
  13. if(STRCMP(s,==,"char"))
  14. return TYPE;
  15. if(STRCMP(s,==,"singed"))
  16. return TYPE;
  17. if(STRCMP(s,==,"unsinged"))
  18. return TYPE;
  19. if(STRCMP(s,==,"short"))
  20. return TYPE;
  21. if(STRCMP(s,==,"int"))
  22. return TYPE;
  23. if(STRCMP(s,==,"long"))
  24. return TYPE;
  25. if(STRCMP(s,==,"float"))
  26. return TYPE;
  27. if(STRCMP(s,==,"double"))
  28. return TYPE;
  29. if(STRCMP(s,==,"struct"))
  30. {
  31. check_type_or_id(s);
  32. return TYPE;
  33. }
  34. if(STRCMP(s,==,"union"))
  35. {
  36. check_type_or_id(s);
  37. return TYPE;
  38. }
  39. if(STRCMP(s,==,"enum"))
  40. {
  41. check_type_or_id(s);
  42. return TYPE;
  43. }
  44. return IDENTIFIER;
  45. }

5.4、解析字段類型:check_type_or_id();

對於類型struct/type/enum,在聲明該類型變量時,類型後的字段極有可能是該關鍵字後可選的"結構標簽"。如聲明struct student xxx;,student是作為一個結構標簽存在。該聲明與struct student {內容…}xxx;一致。所以在判斷student時,需要看它後面字段的類型。如果struct後兩個字段都為標識符,則最後一個標識符才是真的標識符,類型struct/type/enum後的字段則是該類型的另一個名字,如:xxx是一個叫student的結構體。

在該模塊的實現上,則是在讀取到結構struct/type/enum時,再讀取其後的兩個標簽,再判斷,並將真正的標識符及其後的內容返回到輸入流中。

  1. void check_type_or_id(char *s)
  2. {
  3. char temp[MAXTOKENLEN] = {‘\0‘};
  4. struct token temp_struct_one = thistoken;
  5. gettoken();
  6. struct token temp_struct = thistoken;
  7. gettoken();
  8. struct token temp_struct3 = thistoken;
  9. if(thistoken.type==IDENTIFIER)
  10. {
  11. strcat(temp,temp_struct_one.string);
  12. strcat(temp," called ");
  13. strcat(temp,temp_struct.string);
  14. strcpy(s,temp);
  15. thistoken = temp_struct3;
  16. strcpy(temp_struct_one.string,temp);
  17. }
  18. else
  19. {
  20. thistoken = temp_struct;
  21. for(int i = strlen(temp_struct3.string)-1;i>=0;i--)
  22. {
  23. ungetc(temp_struct3.string[i],stdin);
  24. }
  25. }
  26. if(thistoken.type>=0 && thistoken.type<=2)
  27. {
  28. for(int i = strlen(thistoken.string)-1;i>=0;i--)
  29. {
  30. ungetc(thistoken.string[i],stdin);
  31. }
  32. }
  33. thistoken = temp_struct_one;
  34. }

5.5、聲明的處理:deal_with_declarator();

在確定了標識符之後,我們就可以處理各種聲明、修飾符了。依據優先級規則,我們先需要觀察標識符後的符號,以確定其是否是數組/函數;其後還需要處理指針,最後再處理先前被壓棧的符號。

在開始該階段的處理之前,我們觀察read_to_first_identifier()函數,在該函數的最後一行,我們確定了標識符後,有進行了一次gettoken(),這次調用即將標識符後的符號讀入,因此現在在函數開頭我們就可以使用switch()直接選擇要處理的情況。

  1. void deal_with_declarator()
  2. {
  3. switch(thistoken.type)
  4. {
  5. case ‘[‘:
  6. deal_with_arrays();
  7. break;
  8. case ‘(‘:
  9. deal_with_function_args();
  10. break;
  11. }
  12. deal_with_pointers();
  13. while(top >= 0)
  14. {
  15. if(stack[top].type == ‘(‘)
  16. {
  17. pop;
  18. gettoken();
  19. deal_with_declarator();
  20. }
  21. else
  22. {
  23. printf("%s ",pop.string);
  24. }
  25. }
  26. }

5.6、函數參數的處理:deal_with_function_args();

在《C專家編程》中,沒有對函數參數進行處理。在此,我加入了對參數的簡單處理。簡單處理也即,對於復雜聲明的參數,並沒有能正確的處理。在我寫這個模塊時,我有一種對整個程序重構的想法,即將聲明的解析抽象成一個獨立的函數,現在程序裏全局變量對函數功能的拓展限制太大了。

該函數的流程則是,將括號內的字段全部讀取並輸出,遇到‘,‘重新讀取輸出。普通單一的類型說明符可直接輸出(如 int a),而int *a;則無法如此簡單處理。由於輸出使用的是英語,所以該函數大部分的代碼都是在處理不同參數時英語表述的語法問題,如但單參數的‘parameter is‘與多參數的‘parameters are‘等語法細節。處理粗糙,不看也罷。

  1. void deal_with_function_args()
  2. {
  3. char str[MAXTOKENLEN] = {‘\0‘};
  4. char para[MAXTOKENLEN] = {‘\0‘};
  5. bool flag_no_para = true;
  6. bool para_is_one = true;
  7. strcat(str,"function");
  8. gettoken();
  9. if(thistoken.type != ‘)‘)
  10. {
  11. strcat(str," whose parameter");
  12. flag_no_para = false;
  13. }
  14. while(thistoken.type != ‘)‘)
  15. {
  16. if(thistoken.string[0] == ‘,‘)
  17. {
  18. if(para_is_one == true)
  19. {
  20. strcat(str,"s are");
  21. para_is_one = false;
  22. }
  23. strcat(para," and");
  24. }
  25. else
  26. {
  27. strcat(para," ");
  28. strcat(para,thistoken.string);
  29. }
  30. gettoken();
  31. }
  32. if(para_is_one == true && flag_no_para== false)
  33. {
  34. strcat(str," is");
  35. }
  36. strcat(str,para);
  37. gettoken();
  38. if(flag_no_para == true)
  39. {
  40. strcat(str," returning ");
  41. }
  42. else
  43. {
  44. strcat(str,",it returns ");
  45. }
  46. printf("%s",str);
  47. }

六、一些聲明的解析結果

技術分享圖片

技術分享圖片

技術分享圖片

七、寫在後面

新增的功能並不盡如人意,不過也將這次的修改探索總結出來,以供後來者學習,希望後來者少踩一些坑,老老實實重構去哈哈哈。

最後….預祝新年快樂~

源碼地址:C語言聲明解析器修改版源碼

[C語言]聲明解析器cdecl