C编译器剖析_4.2 语义检查_表达式的语义检查(4)_函数调用

4.2.4 函数调用的语义检查

     在这一小节中,我们来讨论一下函数调用的语义检查,语法上,函数调用对应的表达式属于后缀表达式PostfixExpression,UCC编译器exprchk.c的函数CheckFunctionCall()完成了对函数调用的语义检查,如图4.2.18所示。在阅读这份代码时,需要对语法分析后为函数调用构造的语法树有较好认识,请先参照”图3.1.21后缀运算符对应的语法树”或者先预览一下图4.2.19。

 

图4.2.18 CheckFunctionCall()

    对形如f(a,b,c)的函数调用进行语义检查时,我们需要先查找符号表,看看函数f是否已经声明过,图4.2.18第6和第7行进行了这个判断。按C标准的规定,如果f未经声明就使用,则将函数f视为旧式风格的函数声明,相当于f的声明为int f()。我们在“2.4节C语言类型系统”时已做过介绍,旧式风格函数会带来令人抓狂的噩梦。这也应是C++编译器禁止“函数未声明就使用”的原因。从这一点,我们也能再次体会,C++并非是C语言的超集,C++只是尽可能地去兼容已有的C,对于实在看不下去的部分也采取了“摒弃”的策略。第8行的DefaultFunctionType则代表“形如int f()的旧式风格函数声明所对应的类型”,第9至11行把这个隐式声明的int f(),通过函数AddFunction添加到全局符号表中。当然,如果我们之前已经对函数f做了形如int f(int,int,int)的声明,此时就能在符号表中找到函数f的相关信息,或者当我们是通过形如(*ptr)(a,b,c)的方式来调用函数,此时表达式(*ptr)不是op域为OP_ID的表达式结点,则在第13行调用CheckExpression函数来对表达式f或者(*ptr)进行语义检查。经过第15行的Adjust()函数的类型调整后,如果“f或者(*ptr)对应结点”的类型不是“指向函数”的指针,那我们在第18至20行进行错误处理;否则,我们在第22行记下函数的类型信息。对于图4.2.19中的f对应的结点来说,其类型为“指向函数int(int,int,int)”的指针,即int(*)(int,int,int),因此,我们在图4.2.18第22行记下的就是形如int(int,int,int)的类型信息,第56行则记下了函数返回值的类型,即表达式f(30,40,50)的类型为int。关于函数类型的数据结构,请参考第2章的”图2.4.9 函数的类型结构”。

     图4.2.18第24至39行用于对函数调用中的各个实参进行检查,主要的工作由第30行的CheckArgument()来处理,检查的内容包括实参个数是否与形参个数吻合,实参与形参在类型上是否匹配,这相当于要检查能否把实参赋值给形参。第40至55行在实参个数和形参个数不一致时,会给出警告或者错误提示。对于形如int f()的旧式风格的函数声明来说,形参列表并不是其函数接口的一部分,即第42和50行的hasProto为0(不存在原型prototype)。原型prototype的意思是“这是范本,我们得依样画葫芦,范本有几个形参,调用时就得有几个实参”,此时我们仿照Clang编译器的做法,给出警告,如第47和53行所示。对于形如int f(int ,int,int)的新式风格函数声明,我们就得照着原型来进行函数调用了,不然就要报错了,如第44和51行所示。图4.2.18第36至39行的代码用于检查多余的实参,例如函数调用f(30,40,50,60,70),因为在新式风格的声明int f(int,int,int)中,我们只声明了3个形参。第30行的CheckArgument()在处理新式风格函数的参数时,如果发现已经检查完3个实参了,就会置第27行的变量argFull为1,此后不必再执行第29行的while循环。由于语义检查时,语法树结点会发生变化,甚至是重新构建,所以我们要在第30行在记录下CheckArgument()的返回值,而对于多余的实参,则只是在第37行调用CheckExpression()检查一下,此时实际上已经遇到“形参个数与实参个数不一致”的错误了。


图4.2.19 函数调用的语法树

    接下来我们来分析一下图4.2.18第30行的CheckArgument函数,如图4.2.20所示。第5行用于获取新式风格的函数的形参个数,第6行仍然是递归地调用CheckExpression函数进行实参表达式的语义检查,如果新式风格的函数声明形如f(void),即不存在参数,则在第8行设置相应标志位为1,表示对新式风格的函数实参已检查完毕,然后从第9行直接返回。对于形如f(int,int,int)的函数声明来说,f不是变参函数,若当前要检查的实参是最后一个,则第11行的条件成立,此时在第12行置相应标志位为1。而对于形如int f()的旧式风格的函数来说,形参列表并不是函数接口的一部分,我们需要对函数调用中的每个实参进行实参提升的操作,这由第15行的PromoteArgument()函数来完成。对于新式风格的函数,我们需要检查一下实参能否赋值给对应的有名形参,第20行的CanAssign()函数用来做这个判断。出于内存对齐的考虑,C编译器通常会把小于int类型的实参(例如char或者short)转换成int后入栈,第23行执行了这个由编译器隐式进行的转型操作。但是,对于新式风格函数中的float类型实参,并没有进行提升到double类型的动作,这与PromoteArgument()是有所区别的。因为进行过第20行的CanAssign()的判断,所以在第25行,我们在进行实参给形参赋值时,可以进行安全的类型转换。第28行则是用于处理新式风格函数中的变参函数,用于对“无名参数”进行实参提升的操作。阅读CheckArgument()的代码时,若对函数的类型结构不是太清楚,请参考”图2.4.9 函数的类型结构”。


图4.2.20 CheckArgument()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值