C编译器剖析_4.2 语义检查_表达式的语义检查(1)

     在这一节中,我们来分析一下exprchk.c中的代码。我们通过调用图4.2.1中的函数CheckExpression(),来对表达式所对应的语法树结点进行语义检查。虽然表达式结点对应的kind域皆为NK_Expression,但它们的运算符op域还是各有区别的,由此我们可以为不同的运算符表达式构造不同的语义检查函数,然后把这些函数放置在一张函数表ExprCheckes中,用运算符op作为下标,去调用相应的语义检查函数,如图第3行所示。


图4.2.1 CheckExpression()

    图4.2.1第5至12行的代码构造了用于对表达式进行语义检查的函数表ExprCheckers,为了清楚起见,我们通过调用第13行的命令,把经预处理后的得到的函数表列在第14至32行。第10行通过包含头文件opinfo.h,引入了与运算符有关的信息,这些信息如第5行和第9行所示,OP_COMMA对应的是运算符op,接下来的1代表运算符的优先级prec为1,紧接着的”1”是用字符串表示的运算符名称name,跟随其后的Comma是函数名func,而最后的NOP对应的是运算符编码opcode,会用于UCC中间代码中。头文件opcode.h列出了UCC编译器所有中间代码的运算符编码,我们会在讨论中间代码时再进行分析。第15行的 CheckCommaExpression用于对逗号表达式进行检查,而第21行的CheckBinaryExpression则用于对二元运算符表达式进行检查。C语言中的二元运算符较多,为了避免编写包含较多case的swich语句,UCC编译器仍然采用函数表的方式来处理二元运算符,如图4.2.2所示。

 

图4.2.2 CheckBinaryExpression()

     图4.2.2第1126行的CheckLogicalOP用于对“逻辑或”和“逻辑与”进行语义检查,第1141行的CheckMultiplicativeOP用于对乘、除和取余运算进行语义检查。对于形如”a+b”的二元运算表达式而言,都有一个二元运算符和两个操作数,第1153行递归地调用CheckExpression(expr->kids[0])对左操作数进行语义检查,而第1154行则递归地CheckExpression(expr->kids[1])对右操作数进行语义检查,第1155行则查函数表BinaryOpCheckers,来对整个二元表达式a+b进行语义检查。对二叉树如此语义检查的次序,显然属于二叉树的后根遍历。按照C的语义,数组类型和函数类型的操作数参与运算时,需要进行类型的调整,第1153行的函数Adjust则用于此目的,如图4.2.3所示。第1558至1562行用于把“函数类型”转换为“指向函数的指针类型”,因为转换后的类型为指针类型,所以在第1516行会把isfunc置为1,用来标记这个语法树结点原来对应的是一个函数,而非指向函数的指针。


图4.2.3 Adjust()

    例如对于以下代码,通过函数定义,我们在符号表中记录的函数名f的类型为“函数类型”,但在语句“FUNCPTRptr = f;”中,则要对表达式f进行调整,使其类型为“指向函数的指针”。

void f(void){

}

typedef void (*FUNCPTR)(void);

FUNCPTR ptr = f;

    图4.2.3第1547行的参数rvalue为1时,表示我们可把参数expr当右值对待,由第1555行的语句,我们把该语法树结点expr的lvalue域置为0,表示这个结点不是左值,而是充当右值使用。例如对于如下代码,其中变量a和b都存放在“可读可写并且可供C程序员寻址”的内存单元中,所以它们都是左值,但在进行语义检查时,UCC编译器认为,b所对应的表达式结点位于赋值运算符的右侧,所以此时可以把b所对应的语法树结点视为右值,此时调用Adjust()函数时,参数rvalue即为1,表示可把b当右值处理。在C语言中,右值意味着“不可寻址或者只读可不写”,不可寻址的原因在于“这个值可能只是在一个临时变量或者一个寄存器中”,例如表达式”a+b”的值就存放在临时变量中,而只读的原因则往往是由于C程序员使用了const限定符。

int a,b,c;

a = b;

c = a+b;

    当然,不少编译器也把字符串常量隐式地定义为const。不过根据ucc\ansi_C_reference.txt中3.1.4 String literals这一节,“If theprogram attempts to modify a string literal of either form, the behavior isundefined”,即C89标准并未明确规定字符串是否可被修改。例如以下看起来有点奇怪的代码可通过GCC、Clang和UCC的检查,但在GCC和Clang所生成的汇编代码中,会把字符串”123”置于只读区域,这导致在运行时会出现段错误(Segmentation fault),因为语句"123"[0]= 'a';试图去改写只读区域。而UCC生成的汇编代码中,并未把字符串”123”置于只读区域,所以可正常运行。以下这段代码并没有什么实际意义,我们只是用它来说明C标准并没有面面俱到,很多细节还是与语言的实现相关的,即与具体的C编译器相关的。

         int main(void){

         "123"[0]= 'a';

         return0;

}

    图4.2.3第1565行则用于把“数组类型”调整为“指向数组元素的指针类型”。因为数组不能充当左值,例如以下”arr = 0;”是非法的,第1575行把相应语法树结点的lvalue域置为0,表示该结点为右值。对数组进行类型调整后,通过其类型,我们已无法知道其原先为数组,所以第1576行把结点的isarray置为1,用来标记该语法树结点对应的是数组。对于以下代码,通过对声明”int arr[3]”的类型检查,我们会得到arr的类型为“int [4]”,我们会把这个类型信息记录到符号表中。在对表达式”arr+1”进行语义检查时,我们需要先查符号表,看看能否在符号表中查找到名为arr的变量,此处查表可得arr的类型为”int [4]”,经达Adjust()的调整,arr所对应的语法树结点的类型被调整为”int *”,但需要注意的是,符号表中的arr的类型信息仍然是”int [4]”。

int arr[3];

int * ptr = arr+1;

arr = 0;

    在1.5节时,我们介绍过,C语言的数组名和函数名会被特殊处理,我们在本小节讨论的Adjust()函数就是用于此目的。结合2.4节我们给出的类型结构图,我们不难读懂第1565行调用的PointerTo()和Qualify()函数。例如对于如下代码,arr是个数组,const说明整个数组的内容为只读,经过Adjust()类型调整后,我们期望arr[3]中arr对应的语法树结点类型为const int * ,所以我们需要在图4.2.3第1553行先记录下这个限定符const,然后在第1565行的Qualify()和PointerTo()函数构建出类型const int*。由此,arr[3]对应的语法树结点的类型则为const int,当语句”arr[3]=5;”试图改变arr[3]时,则因无法通过类型系统的检查,会报错” error:Theleft operand cannot be modified”。

typedef int ARRAY[4];

const ARRAY arr = {11,12,13,14};

int main(){

         arr[3] = 5;

         return 0;

}

    在这一小节中,我们介绍了对表达式进行语义检查所用到的ExprCheckes和BinaryOPCheckers这两张函数表,及用于对函数和数组表达式结点进行类型调整的函数Adjust()。在调用Adjust()函数时,我们可根据调用时的上下文来决定相应的表达式结点是否要当右值使用,当Adjust()函数的参数rvalue为1时,可把结点当右值用,否则不改变其左值或右值的属性。这两张函数表中的具体函数,我们会在后面的小节中进行分析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值