4.2.5 成员选择运算符
在C语言中,结构体struct和联合体union被称为记录类型RecordType,在形如dt.a和ptr->a的后缀表达式中,运算符.和->被称为成员选择运算符。函数CheckMemberAccess()用于对这些表达式进行语义检查,与之相关的代码如图4.2.28所示。在表达式dt.a中,dt和a相当于是运算符.的两个操作数,dt对应的语法树结点的类型应是记录类型,图4.2.28第8行对此进行了判断;而在形如ptr->a的表达式中,ptr对应的结点类型应为“指向记录”的指针,第15行的if语句对此进行了检查。第4行用于对成员选择运算符的左操作数进行语义检查,结构体对象dt可当作左值,由此dt.a也可充当左值,但由于函数的返回值只存放在临时变量中,不可充当左值,所以GetData().a也不可充当左值,第11行对此进行了设置。对ptr->a来说,左操作数不必是左值,即使是个临时变量也可,例如在表达式((Data
*)ptr2)->a中,强制转型后的结果((Data*)ptr2)就存放在一个临时变量中,但ptr或((Data *)ptr2)的值代表的是一个可寻址内存单元的地址,所以我们可把ptr->a当作左值来用,第19行完成了这个设置。当然,即便表达式dt.a或者ptr->a对应的语法树结点的lvalue域为1,在对dt.a或者ptr->a进行赋值时,我们还会检查一下其类型信息,看看是否有const限定符,如第27行的注释所示,如果定义dt变量时有const的限定,那么dt.a也相当于有const限定符,第28行的Qualify()函数用于添加这个类型限定符。我们还需要在检查一下结构体Data中是否有域成员a,第21行的LookupField(Type
ty,char *id)函数用于此目的。给合我们在第2章给出的“图2.4.4 结构体的类型结构”,理解这个函数不会太难。如果在结构体的定义中找不到成员a,则说明dt.a是非法的表达式,则在第23行报错。我们把查询得到的关于域成员a的类型信息通过第29行的代码,存放到dt.a表达式对应的语法树结点上。与第3行的Field对应的结构体struct filed描述了域成员的相关信息,我们在第2章的“图2.4.3结构体的类型描述”中介绍过这个结构体,这里不再重复。
图4.2.28 CheckMemberAccess()
图4.2.29给出了表达式dt.a在语法分析后和语义检查后所对应的语法树,对op域为OP_ID的语法树结点的语义检查是由exprchk.c中的CheckPrimaryExpression函数来完成的,在语义检查后,域成员a在结构体struct Data中的偏移量offset等信息由图中的struct filed对象保存,而结构体对象dt对应的符号则由struct
symbol对象来存放。
图4.2.29 成员选择运算的语法树
接下来我们来看一下对“后加加”和“后减减”表达式的语义检查,例如a++和a--。与其相关的代码如图4.2.30所示。由图4.2.30第396和397行的注释,我们可以知道,UCC编译器会把a++和++a转换成a+=1来处理,而把a--和--a转换成a-=1来处理,第394行创建了一个语法树结点,第398行置其op域为OP_ADD_ASSIGN或OP_SUB_ASSIGN,即+=或者-=,在第402行我们调用CheckExpression()来对赋值运算符+=或者-=进行语义检查。需要注意的是,结点expr的op域仍然保存着原有的“后加加OP_POSTINC”或“后减减OP_POSTDEC”等运算符,这样在生成代码时,我们能为a++、a--、++a或者--
a进行区别,进而产生不同的代码。
图4.2.30 TransformIncrement()
至此,历经一番曲折,我们已经完成了对所有后缀表达式的语义检查。在后续章节中,我们会对一元运算表达式和二元运算表达式进行语义检查。沉舟侧畔千帆过,让我们继续扬帆前行。