C编译器剖析_2.2 词法分析

本文深入探讨了UCC编译器中的词法分析过程,包括从文件读取到内存处理,以及如何使用宏和结构体进行错误定位和报告。详细介绍了词法分析器如何通过switch语句和扫描函数表进行字符分类处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2.2  词法分析

    目录ucc\ucl下,与词法分析相关的C文件主要有input.c和lex.c,input.c用于从外存读入预处理后的文件,其主要的函数如图2.2.1所示。在UCC驱动的代码中,已经预定义了宏_UCC,所以第39行的条件成立,函数ReadSourceFile()会使用C标准库的IO函数来读取文件。因为.c文件只是普通的文本文件,其文件大小很少超过1M字节的,所以UCC并没有采取逐行读的行缓存策略,而是一下把整个文件读入内存中,第45行的fopen()用于打开文件,通过第56行的fseek()函数和第57行的ftell()函数,就可以获得文件的大小,然后通过第59行的malloc()开辟了足够大的内存堆空间,第67行通过fread()把整个文件读入内存中。

    

图2.2.1 ReadSourceFile()

     此后,文件lex.c的词法分析器就不再需要去读写文件了,只需要从内存中读取字符即可。头文件ucc\ucl\input.h中的结构体structinput记录了关于输入文件的信息,如图2.2.2所示,其中第15行的base域记录了读入的输入文件在内存中的起始位置,而cursor则反映了当前的读取位置。对一个名为hello.c的C文件而言,UCC编译器见到的实际上经预处理后的文件hello.i。文件hello.i包含了各个头文件及C文件hello.c,为了在错误处理时能准确报告出错位置,引入了图2.2.2第4行的结构体struct coord。其中,coord是coordinate的缩写,代表“坐标”之意,其中包含了出错的原始文件名filename,比如hello.c;出错的行号ppline,这个行号指的是在hello.c中的行号;而line代表的在预处理后的文件hello.i中的行号;出错的列号col。


                                               图2.2.2 struct input

    在ucc\ucl\error.c的Do_Error()函数中,我们会看到如下用于报告出错位置的代码。

void Do_Error(Coord coord, const char *format, ...){

         ……

fprintf(stderr, "(%s,%d):",coord->filename, coord->ppline);

……

}

    在UCC源代码中,我们使用Error()来报错,而不是直接使用Do_Error(),Error的宏定义如下所示。这么做的主要目的是为了方便UCC编译器的调试。

#define     Error         fprintf(stderr, "(%s,%d):",__FILE__, __LINE__) , Do_Error

    通过Error(),我们可以得到如下所示的出错提示,(hello.c,3)告诉我们,程序员编写的hello.c的第3行缺少了一个分号,而我们是在UCC编译器源代码的stmt.c的第24行检测到这个错误的。当然,真正把UCC编译器作为一个产品发布时,” (stmt.c24):”这样的信息是没有必要打印出来的。此时,只要把Error的宏定义改为” #define         Error  Do_Error ”即可。

(stmt.c 24):(hello.c,3):error:Expect ;

     接下来,我们来分析一下词法分析器lex.c中的代码。因为一个字符对应一个字节,一个字节包含了0至255种不同的数值,所以我们可以写一个大大的switch语句,根据当前字符的值来分门别类地进行处理,如下所示。

switch(*cursor){

case  0:           ….     ;        break;

case  1:          ….     ;        break;

…..

case   255:               ….     ;        break;

}

   不过,一般情况下,当遇到一个switch语句中有这么多的case时,往往会采取“打表”的方法来简化。图2.2.3是UCC的词法分析器GetNextToken的主要代码,每调用一次GetNextToken()我们就会获得一个由若干个字符构成的单词,第1165行的SkipWhiteSpace()跳过了注释、空格、制表符、回车和换行等空白符,真正进行词法分析的是第1170行的代码。文件ucc\ucl\token.h中包含了UCC编译器用到的各个token名称。

enum token

{

         TK_BEGIN,

#define TOKEN(k, s) k,

#include "token.h"

#undef  TOKEN

 

};


                                               图2.2.3   GetNextToken()

    我们用CURSOR所指向的当前字符值来作为数组Scanners的下标,字符(*CURSOR)的值正好在0至255之间,而数组Scanners即为一个由函数指针构成的表格。

typedef int (*Scanner)(void);

static Scanner       Scanners[256];

    这个函数指针构成的表格Scanners当然需要经过初始化,在main()函数中,我们调用了SetupLexer()来初始化词法分析器,其主要的工作就是初始化这个表格,如图2.2.4所示。


图2.2.4 SetupLexer()

     该表格共有256项,各个以”Scan”为前缀命名的扫描函数的代码不会太复杂,我们就不一一进行分析,仅以扫描标志符为例,如图2.2.5所示,第778至788行处理以字母L开始的形如L’a’的宽字符和形如L”abc”的宽字符串。第796行需要再判断一下识别出来的标志符会不会是C语言的关键字,如果确实是C程序员声明的标志符(变量名或者函数名),我们就在TokenValue中记录其名称。


图2.2.5 ScanIdentifier()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值