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个函数的实现过程。