- 編譯系統透視:圖解編譯原理
- 新設計團隊
- 4258字
- 2019-01-04 03:30:37
2.4.2 結合GCC源代碼講解詞法分析過程
1.分析空白分隔符
空白分隔符不屬于C語言中符號的實際內容,遇到它后,會直接將其當作空白字符跳過,即跳轉到skipped_white這個點,回到初始狀態,情景如圖2-42所示。

圖2-42 空白分隔符的狀態轉換圖
代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的字符,準備分析 { case ' ': case '\t': case '\f': case '\v': case '\0': // 這幾個字符就是空白分隔符 result->flags |= PREV_WHITE; // 設置空白分隔符標志 skip_whitespace (pfile, c); // 發現空白字符后一直往后遍歷,直到找到 // 非空白分隔符為止 goto skipped_white; // 回到初始狀態,準備繼續分析下一個字符 …… }
2.分析換行符
換行符也不屬于實際的符號內容,從代碼中可以看到,沒有對這個字符做什么符號屬性標記,遇到它后跳轉到fresh_line這個點,回到初始狀態,情景如圖2-43所示。

圖2-43 換行符的狀態轉換圖
代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的字符 { …… case '\n': // 遇到換行符 …… goto fresh_line; // 跳轉到這個點,回到初始狀態,準備繼續 // 分析下一個字符 …… }
3.確定符號為數字
根據C語言規則,如果符號的第一個字符是0到9中的任意一個,該符號就是數字,如果第一個字符是“.”,就要看緊跟著的字符,如果仍是0到9中的任意一個,也表示該符號是數字,是浮點型數字。至于具體的情景,我們在介紹完_cpp_lex_direct函數后會詳細介紹,代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的字符“{” …… case '0': case '1': case '2': case '3': case '4': // 遇到這幾個字符就確定當前符號是數字 case '5': case '6': case '7': case '8': case '9': { …… result->type = CPP_NUMBER; // 將當前符號屬性設置為數字 lex_number (pfile, &result->val.str, &nst); // 此函數將數字的全部內容先以字符串的形式 // 存儲起來,以待分析 …… break; } …… case '.': // 識別到“.” …… if (ISDIGIT (*buffer->cur)) // 檢測后面跟隨的是不是0到9中的字符 { …… result->type = CPP_NUMBER; // 將符號屬性設置為數字 lex_number (pfile, &result->val.str, &nst); // 此函數將數字的全部內容先以字符串的形式 // 存儲起來,以待分析 …… } else if (*buffer->cur == '.' && buffer->cur[1] == '.') // 再前瞻兩個字符,如果都是“.” buffer->cur += 2, result->type = CPP_ELLIPSIS; // 將符號屬性設置為缺省符 …… }
4.確定符號為字符或字符串
具體的情景我們在介紹完_cpp_lex_direct函數后會詳細介紹,這里先總體看一下。如果符號的第一個字符如果是“L”、“u”、“U”或“R”,可能是標識符,也可能是字符串,程序中先分析它是不是字符串,代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的字符 { …… case 'L': // 識別到“L”、“u”、“U”或“R”,先分析 // 是不是字符或字符串 case 'u': case 'U': case 'R': /* 'L', 'u', 'U', 'u8' or 'R' may introduce wide characters, wide strings or raw strings. */ if (c == 'L' || CPP_OPTION (pfile, rliterals) || (c != 'R' && CPP_OPTION (pfile, uliterals))) { if ((*buffer->cur == '\'' && c != 'R') // cur指向的是符號第二個字符,cur[1]和cur[2] || *buffer->cur == '"' // 分別是第三個和第四個字符,這里以前瞻的 || (*buffer->cur == 'R' // 方式來判斷符號是不是字符或字符串 && c != 'R' && buffer->cur[1] == '"' && CPP_OPTION (pfile, rliterals)) || (*buffer->cur == '8' && c == 'u' && (buffer->cur[1] == '"' || (buffer->cur[1] == 'R' && buffer->cur[2] == '"' && CPP_OPTION (pfile, rliterals))))) { lex_string (pfile, result, buffer->cur - 1); // 如果是字符或字符串,進入這個函數,具體分析 // 它們的內容 break; } } /* Fall through. */ case '_': // 執行到這里,說明不是字符串,而是標識符,則處理標識符 case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':…… }
符號的第一個字符如果是“\'”或“"”,就可以確定符號是字符或字符串,進入lex_string函數來分析字符串內容。代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的ASCII碼,準備分析 { …… case '\'': // 識別到“\'”或“"”,確定當前符號是字符串 case '"': lex_string (pfile, result, buffer->cur - 1); // 確定是字符串后,進入此函數,具體分析字符串的內容 break; …… }
5.確定符號為標識符
下面我們來看標識符的識別,至于具體的情景,我們在介紹完_cpp_lex_direct函數后會詳細介紹,先總體看一下。對于前面介紹的識別字符串,在識別到“L”、“u”、“U”或“R”時,如果當前符號不是字符串,那就是標識符,標識符的第一個字符是字母或下劃線,這幾個字符屬于字母。我們來看完整的標識符識別條件,代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的ASCII碼,準備分析 { …… case 'L': // 識別到“L”、“u”、“U”或“R”,先分析是不是字符串 case 'u': case 'U': case 'R': /* 'L', 'u', 'U', 'u8' or 'R' may introduce wide characters, wide strings or raw strings. */ if (c == 'L' || CPP_OPTION (pfile, rliterals) // 如果不是字符或字符串,則是標識符 || (c != 'R' && CPP_OPTION (pfile, uliterals))) { if ((*buffer->cur == '\'' && c != 'R') || *buffer->cur == '"' || (*buffer->cur == 'R' && c != 'R' && buffer->cur[1] == '"' && CPP_OPTION (pfile, rliterals)) || (*buffer->cur == '8' && c == 'u' && (buffer->cur[1] == '"' || (buffer->cur[1] == 'R' && buffer->cur[2] == '"' && CPP_OPTION (pfile, rliterals))))) { lex_string (pfile, result, buffer->cur - 1); // 如果是字符或字符串,進入這個函數,具體分析它們的內容 break; } } /* Fall through. */ case '_': // 字母或下劃線開頭的符號都是標識符 case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'v': case 'w': case 'x': case 'y': case 'z': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'S': case 'T': case 'V': case 'W': case 'X': case 'Y': case 'Z': result->type = CPP_NAME; // 設置符號屬性為標識符 { …… result->val.node.node = lex_identifier (pfile, buffer->cur - 1, false,&nst); // 確定符號為標識符后,進入此函數,繼續識別標識符的內容 …… } /* Convert named operators to their proper types. */ …… break; …… }
6.分析運算符和分隔符
下面我們來看各種運算符和分隔符。
先看符號“/”,它后面緊跟不同的字符來構成不同的運算符。“/*”和“//”都是注釋的意思,“/*”和“*/”組合時可以注釋掉它們之間的內容;“//”用來注釋掉當前行的內容;“/=”是指對運算符左邊和右邊的操作數做除法,然后將數值賦給左邊操作數;不是以上情況才能確定“/”就是除號,情景如圖2-44所示。

圖2-44 “注釋”、“/=”和“/”的狀態轉換圖
代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的ASCII碼,準備分析 { …… case '/': // 識別到“/” …… c = *buffer->cur; // 獲取“/”后面的字符,準備繼續往后識別,看是不是除號 if (c == '*') // 識別到后面是“*”,確定是注釋一些內容 { if (_cpp_skip_block_comment (pfile)) // 進入該函數,一直遍歷后續的ASCII碼,看能不能找到 // “*/”,確定注釋掉哪部分內容,這些內容就不參與詞法分析了 …… } else if (c == '/' && (CPP_OPTION (pfile, cplusplus_comments) || cpp_in_system_header (pfile))) // 識別到“/”,確定是要注釋掉當前行中的內容 { …… if (skip_line_comment (pfile) && CPP_OPTION (pfile, warn_comments)) // 一直遍歷ASCII碼,找到“\n”,確定一行結束, // 此行的內容不再參與詞法分析 …… } else if (c == '=') // 識別到“=”,確定是“/=”運算符 { buffer->cur++; // cur指向“=”后面的字符,準備識別后面的符號 result->type = CPP_DIV_EQ; // 設置當前符號屬性為“/=” break; } else { result->type = CPP_DIV; // 后面跟的字符不是以上三種情況,說明當前符號就是 // 除號,設置屬性 break; } …… }
下面來看“<”。如果此時正在進行預處理,“<”可以用來引用某個頭文件,它就不是小于號,否則,還要看它后面跟著什么字符,如果后續是“=”,則構成“<=”運算符,即小于等于;如果后續仍然是“<”,就還要往后看,如果后續仍然是“=”,就是“<<=”,即左移并賦值,如果后續沒有“=”,就是“<<”,即左移;這些情況都排除掉,才可以確定“<”就是小于號,情景如圖2-45所示。

圖2-45 “<=”、“<<=”、“<<”、“<”的狀態轉換圖
代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的ASCII碼,準備分析 { …… case '<': // 識別到“<” if (pfile->state.angled_headers) // 此時正處于預處理階段,要確定頭文件了 { lex_string (pfile, result, buffer->cur - 1); // 分析頭文件的字符串信息 if (result->type != CPP_LESS) break; } result->type = CPP_LESS; // 默認運算符是小于號,后面如果沒有改變,就是它了 if (*buffer->cur == '=') // 識別到下一個字符號是“=” buffer->cur++, result->type = CPP_LESS_EQ; // 設置當前符號是小于等于號 else if (*buffer->cur == '<') // 識別到下一個字符是“<” { buffer->cur++; // 繼續往后看 IF_NEXT_IS ('=', CPP_LSHIFT_EQ, CPP_LSHIFT); // 分析出究竟是“<<=”還是“<<” } …… break; …… }
大于號的分析識別過程與小于號類似,如果后續是“=”,則構成“>=”運算符,即大于等于;如果仍然跟著“>”,就還要往后看,若后續是“=”,就是“>>=”,即右移并賦值,如果后續沒有“=”,就是“>>”,即右移;這些情況都排除掉,才可以確定“>”就是大于號,情景如圖2-46所示。

圖2-46 “>=”、“>>=”、“>>”、“>”的狀態轉換圖
代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的ASCII碼,準備分析 { …… case '>': // 識別到“>” result->type = CPP_GREATER; // 將屬性默認設置為大于號,后面沒有變動,就是它 if (*buffer->cur == '=') // 識別到后面跟著的字符是“=” buffer->cur++, result->type = CPP_GREATER_EQ; // 設置符號屬性為大于等于 else if (*buffer->cur == '>') // 識別到后面跟著的字符是“>” { buffer->cur++; // 還得往后看 IF_NEXT_IS ('=', CPP_RSHIFT_EQ, CPP_RSHIFT); // 分析出究竟是“>>=”還是“>>” } break; …… }
下面來看“%”,情況比較簡單,識別到“%”后,往后再看一個字符,如果是“=”,就是“%=”,即求余并賦值,否則就是求余,情景如圖2-47所示。

圖2-47 “%=”、“%”的狀態轉換圖
代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的ASCII碼,準備分析 { …… case '%': // 識別到“%” result->type = CPP_MOD; // 默認就是求余運算符,后面沒有改變,就是它 if (*buffer->cur == '=') // 繼續往后識別到“=” buffer->cur++, result->type = CPP_MOD_EQ; // 確定此運算符是“%=” …… break; …… }
下面來看“.”。如果后面跟著的第二個和第三個字符都是“.”,說明當前符號是“...”,即缺省符,表示變參,情景如圖2-48所示。

圖2-48 “…”、“.*”的狀態轉換圖
代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的ASCII碼,準備分析 { …… case '.': // 識別到“.” …… if (ISDIGIT (*buffer->cur)) // 檢測后面跟隨的是不是0到9中的字符 { …… result->type = CPP_NUMBER; // 將符號屬性設置為數字 lex_number (pfile, &result->val.str, &nst); // 此函數將數字的全部內容先以字符串的形式 // 存儲起來,以待分析 …… } else if (*buffer->cur == '.' && buffer->cur[1] == '.') // 再往后識別兩個字符,如果都是“.” buffer->cur += 2, result->type = CPP_ELLIPSIS; // 將符號屬性設置為缺省符 …… }
其他運算符的識別過程大體類似,情景如圖2-49和圖2-50所示。

圖2-49 各種運算符和分隔符的狀態轉換圖一

圖2-50 各種運算符和分隔符的狀態轉換圖二
代碼如下所示:
//代碼路徑:libcpp/lex.c: cpp_token * _cpp_lex_direct (cpp_reader *pfile) { …… switch (c) // c中保存著當前正要分析的ASCII碼,準備分析 { …… case '+': // 識別出字符“+” result->type = CPP_PLUS; // 默認符號屬性是加法運算符 if (*buffer->cur == '+') // 繼續往后識別,又識別到了字符“+” buffer->cur++, result->type = CPP_PLUS_PLUS; // 將符號屬性設置為“++” else if (*buffer->cur == '=') // 繼續往后識別,又識別到了字符“=” buffer->cur++, result->type = CPP_PLUS_EQ; // 將符號屬性設置為“+=” break; case '-': // 識別出字符“-” result->type = CPP_MINUS; // 默認符號屬性是減法運算符 if (*buffer->cur == '>') // 繼續往后識別,識別到了字符“>” { …… result->type = CPP_DEREF; // 確認是“->”運算符,即指向運算符 …… } else if (*buffer->cur == '-') // 繼續往后識別,識別到了字符“-” buffer->cur++, result->type = CPP_MINUS_MINUS; // 確認是“--”運算符 else if (*buffer->cur == '=') // 繼續往后識別,識別到了字符“=” buffer->cur++, result->type = CPP_MINUS_EQ; // 確認是“-=”運算符 break; case '&': // 識別出字符“&” result->type = CPP_AND; // 確定運算符屬性是“&”,即按位與 if (*buffer->cur == '&') // 繼續識別到字符“&”, buffer->cur++, result->type = CPP_AND_AND; // 確定運算符為“&&” else if (*buffer->cur == '=') // 繼續識別到字符“=” buffer->cur++, result->type = CPP_AND_EQ; // 確定運算符為“&=” break; case '|': // 識別出字符“|” result->type = CPP_OR; // 確定運算符屬性是“|”,即按位或 if (*buffer->cur == '|') // 繼續識別到字符“|” buffer->cur++, result->type = CPP_OR_OR; // 確定運算符為“||” else if (*buffer->cur == '=') // 繼續識別到字符“=” buffer->cur++, result->type = CPP_OR_EQ; // 確定運算符為“|=” break; case ':': // 識別出字符“:” result->type = CPP_COLON; // 確定運算符為“:” …… break; case '*': IF_NEXT_IS ('=', CPP_MULT_EQ, CPP_MULT); break; // 確定運算符為“*=”或“*” case '=': IF_NEXT_IS ('=', CPP_EQ_EQ, CPP_EQ); break; // 確定運算符為“==”或“=” case '!': IF_NEXT_IS ('=', CPP_NOT_EQ, CPP_NOT); break; // 確定運算符為“!=”或“!” case '^': IF_NEXT_IS ('=', CPP_XOR_EQ, CPP_XOR); break; // 確定運算符為“^=”或“^” case '#': IF_NEXT_IS ('#', CPP_PASTE, CPP_HASH); result->val.token_no = 0; break; // 確定運算符為“##”或“#” case '?': result->type = CPP_QUERY; break; // 確定運算符為“?” case '~': result->type = CPP_COMPL; break; // 確定運算符為“~” case ',': result->type = CPP_COMMA; break; // 確定運算符為“,” case '(': result->type = CPP_OPEN_PAREN; break; // 確定運算符為“(” case ')': result->type = CPP_CLOSE_PAREN; break; // 確定運算符為“)” case '[': result->type = CPP_OPEN_SQUARE; break; // 確定運算符為“[” case ']': result->type = CPP_CLOSE_SQUARE; break; // 確定運算符為“]” case '{': result->type = CPP_OPEN_BRACE; break; // 確定運算符為“{” case '}': result->type = CPP_CLOSE_BRACE; break; // 確定運算符為“}” case ';': result->type = CPP_SEMICOLON; break; // 確定運算符為“;” …… }
在_cpp_lex_direct函數中,每一次switch...case...都對應著一次狀態轉換圖上的“起始狀態”至“終態”,即識別一個符號。不難發現,各個運算符的識別過程比較簡單,一兩步或兩三步就到終態了,而標識符、數字以及字符串的識別過程相對煩瑣一些,我們在代碼分析中只做了大體介紹。在確定了符號屬性后,后續的內容分別在lex_identifier、lex_number和lex_string函數中實現,最終達到終態。接下來,我們分別詳細講解這3個函數的實現過程。
- FuelPHP Application Development Blueprints
- 算法零基礎一本通(Python版)
- Python爬蟲開發:從入門到實戰(微課版)
- PHP基礎案例教程
- 零基礎玩轉區塊鏈
- 青少年軟件編程基礎與實戰(圖形化編程三級)
- DevOps入門與實踐
- 面向STEM的Scratch創新課程
- Java高手是怎樣煉成的:原理、方法與實踐
- IBM DB2 9.7 Advanced Application Developer Cookbook
- Visual C++ 開發從入門到精通
- 軟件自動化測試實戰解析:基于Python3編程語言
- 寫給所有人的編程思維
- 機器學習開發者指南
- MySQL 5.7從入門到精通(視頻教學版)(第2版)