编译系统透视:图解编译原理
上QQ阅读APP看书,第一时间看更新

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))                                // 检测后面跟随的是不是09中的字符
      {
       ……
       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 '&lt;':                                                // 识别到“&lt;
         if (pfile-&gt;state.angled_headers)                // 此时正处于预处理阶段,要确定头文件了
        {
           lex_string (pfile, result, buffer-&gt;cur - 1);        // 分析头文件的字符串信息
           if (result-&gt;type != CPP_LESS)
           break;
        }
       result-&gt;type = CPP_LESS;                                // 默认运算符是小于号,后面如果没有改变,就是它了
       if (*buffer-&gt;cur == '=')                                // 识别到下一个字符号是“=
         buffer-&gt;cur++, result-&gt;type = CPP_LESS_EQ;        // 设置当前符号是小于等于号
      else if (*buffer-&gt;cur == '&lt;')                        // 识别到下一个字符是“&lt;
      {
        buffer-&gt;cur++;                                        // 继续往后看
        IF_NEXT_IS ('=', CPP_LSHIFT_EQ, CPP_LSHIFT);        // 分析出究竟是“&lt;&lt;=”还是“&lt;&lt;
      }
      ……
      break;
    ……
}

大于号的分析识别过程与小于号类似,如果后续是“=”,则构成“>=”运算符,即大于等于;如果仍然跟着“>”,就还要往后看,若后续是“=”,就是“>>=”,即右移并赋值,如果后续没有“=”,就是“>>”,即右移;这些情况都排除掉,才可以确定“>”就是大于号,情景如图2-46所示。

图2-46 “>=”、“>>=”、“>>”、“>”的状态转换图

代码如下所示:

//代码路径:libcpp/lex.c:
cpp_token *
_cpp_lex_direct (cpp_reader *pfile)
{
  ……
  switch (c)                                                // c中保存着当前正要分析的ASCII码,准备分析
    {
       ……
       case '&gt;':                                                // 识别到“&gt;
         result-&gt;type = CPP_GREATER;                        // 将属性默认设置为大于号,后面没有变动,就是它
         if (*buffer-&gt;cur == '=')                        // 识别到后面跟着的字符是“=
           buffer-&gt;cur++, result-&gt;type = CPP_GREATER_EQ;        // 设置符号属性为大于等于
        else if (*buffer-&gt;cur == '&gt;')                        // 识别到后面跟着的字符是“&gt;
        {
          buffer-&gt;cur++;                                        // 还得往后看
          IF_NEXT_IS ('=', CPP_RSHIFT_EQ, CPP_RSHIFT);        // 分析出究竟是“&gt;&gt;=”还是“&gt;&gt;
        }
        break;
      ……
}

下面来看“%”,情况比较简单,识别到“%”后,往后再看一个字符,如果是“=”,就是“%=”,即求余并赋值,否则就是求余,情景如图2-47所示。

图2-47 “%=”、“%”的状态转换图

代码如下所示:

//代码路径:libcpp/lex.c:
cpp_token *
_cpp_lex_direct (cpp_reader *pfile)
{
  ……
  switch (c)                                                // c中保存着当前正要分析的ASCII码,准备分析
    { 
       ……
       case '%':                                                // 识别到“%
        result-&gt;type = CPP_MOD;                                // 默认就是求余运算符,后面没有改变,就是它
         if (*buffer-&gt;cur == '=')                        // 继续往后识别到“=
           buffer-&gt;cur++, result-&gt;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-&gt;cur))                                // 检测后面跟随的是不是09中的字符
          {
           ……
           result-&gt;type = CPP_NUMBER;                                // 将符号属性设置为数字
           lex_number (pfile, &amp;result-&gt;val.str, &amp;nst);                // 此函数将数字的全部内容先以字符串的形式
                                                                        // 存储起来,以待分析
           ……
          }
          else if (*buffer-&gt;cur == '.' &amp;&amp; buffer-&gt;cur[1] == '.')        // 再往后识别两个字符,如果都是“.
             buffer-&gt;cur += 2, result-&gt;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-&gt;type = CPP_PLUS;                                        // 默认符号属性是加法运算符
       if (*buffer-&gt;cur == '+')                                        // 继续往后识别,又识别到了字符“+
          buffer-&gt;cur++, result-&gt;type = CPP_PLUS_PLUS;                // 将符号属性设置为“++
       else if (*buffer-&gt;cur == '=')                                // 继续往后识别,又识别到了字符“=
          buffer-&gt;cur++, result-&gt;type = CPP_PLUS_EQ;                // 将符号属性设置为“+=
       break;
     case '-':                                                        // 识别出字符“-
       result-&gt;type = CPP_MINUS;                                        // 默认符号属性是减法运算符
       if (*buffer-&gt;cur == '&gt;')                                        // 继续往后识别,识别到了字符“&gt;
       {
         ……
         result-&gt;type = CPP_DEREF;                                // 确认是“-&gt;”运算符,即指向运算符
         ……
       }
       else if (*buffer-&gt;cur == '-')                                // 继续往后识别,识别到了字符“-
         buffer-&gt;cur++, result-&gt;type = CPP_MINUS_MINUS;        // 确认是“--”运算符
       else if (*buffer-&gt;cur == '=')                                // 继续往后识别,识别到了字符“=
         buffer-&gt;cur++, result-&gt;type = CPP_MINUS_EQ;                // 确认是“-=”运算符
      break;
    case '&amp;':                                                        // 识别出字符“&amp;
       result-&gt;type = CPP_AND;                                        // 确定运算符属性是“&amp;”,即按位与
        if (*buffer-&gt;cur == '&amp;')                                        // 继续识别到字符“&amp;”,
           buffer-&gt;cur++, result-&gt;type = CPP_AND_AND;                // 确定运算符为“&amp;&amp;
        else if (*buffer-&gt;cur == '=')                                // 继续识别到字符“=
           buffer-&gt;cur++, result-&gt;type = CPP_AND_EQ;                // 确定运算符为“&amp;=
        break;
     case '|':                                                        // 识别出字符“|
        result-&gt;type = CPP_OR;                                        // 确定运算符属性是“|”,即按位或
          if (*buffer-&gt;cur == '|')                                // 继续识别到字符“|
            buffer-&gt;cur++, result-&gt;type = CPP_OR_OR;                // 确定运算符为“||
          else if (*buffer-&gt;cur == '=')                                // 继续识别到字符“=
            buffer-&gt;cur++, result-&gt;type = CPP_OR_EQ;                // 确定运算符为“|=
         break;
      case ':':                                                        // 识别出字符“:
        result-&gt;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-&gt;val.token_no = 0; break;
                                                                        // 确定运算符为“##”或“#
      case '?': result-&gt;type = CPP_QUERY; break;                // 确定运算符为“?”
      case '~': result-&gt;type = CPP_COMPL; break;                // 确定运算符为“~
      case ',': result-&gt;type = CPP_COMMA; break;                // 确定运算符为“,
      case '(': result-&gt;type = CPP_OPEN_PAREN; break;                // 确定运算符为“(
      case ')': result-&gt;type = CPP_CLOSE_PAREN; break;                // 确定运算符为“)
      case '[': result-&gt;type = CPP_OPEN_SQUARE; break;                // 确定运算符为“[
      case ']': result-&gt;type = CPP_CLOSE_SQUARE; break;        // 确定运算符为“]
      case '{': result-&gt;type = CPP_OPEN_BRACE; break;                // 确定运算符为“{
      case '}': result-&gt;type = CPP_CLOSE_BRACE; break;                // 确定运算符为“}
      case ';': result-&gt;type = CPP_SEMICOLON; break;                // 确定运算符为“;
      ……
}

在_cpp_lex_direct函数中,每一次switch...case...都对应着一次状态转换图上的“起始状态”至“终态”,即识别一个符号。不难发现,各个运算符的识别过程比较简单,一两步或两三步就到终态了,而标识符、数字以及字符串的识别过程相对烦琐一些,我们在代码分析中只做了大体介绍。在确定了符号属性后,后续的内容分别在lex_identifier、lex_number和lex_string函数中实现,最终达到终态。接下来,我们分别详细讲解这3个函数的实现过程。