C编译器剖析_4.4 语义检查_外部声明_类型结构的构建(1)

4.4.1  类型结构的构建

    在这一节中,我们要对C语言的外部声明进行语义检查,按C标准文法,我们需要对“函数定义”和“在函数体外出现的变量与函数声明”进行检查,相关代码主要在declchk.c中。每个C文件在文法中被称为翻译单元TranslationUnit,与之相关的语义检查函数如图4.4.1第1至第15行所示。第7行调用的CheckFunction()用于对函数定义进行语义检查,而第10行调用的CheckGlobalDeclaration()用于对“在函数体外出现的变量和函数声明”进行语义检查。第16至33行是CheckGlobalDeclaration函数的核心代码,第34至49行则为CheckFunction的核心代码。对比这两者的代码,我们可以发现它们都依次调用了对声明说明符DeclarationSpecifiers和声明符Declarator进行语义检查的代码。而被声明的标识符的类型信息正好是分布在DeclarationSpecifiers和Declarator中,通过调用第29或46行的DeriveType函数,会把这两部分的类型信息组合到一起,构造完整的类型信息。例如,对于int arr[4]来说,int是其声明说明符,而arr[4]是声明符,只有把int 和[4]组合到一起,我们才能得到arr的类型信息为int [4]。


图4.4.1 CheckTranslationUnit ()

     如果把函数定义基本上可看成是“函数声明+函数体(即复合语句)”,则函数定义的类型信息主要体现在其函数声明部分。UCC编译器declchk.c中的代码通过对声明的语义检查,建立起C语言的类型系统。具体来说,就是创建我们在“第2.4节 C语言的类型系统”中各个示意图中给出的类型结构。我们在第3章给出了“图3.3.4 声明Declaration所对应的语法树”和“图3.3.7函数定义所对应的语法树”。为阅读方便起见,这里我们再次给出这两个图。

        

图3.3.4 声明Declaration所对应的语法树


图3.3.7函数定义所对应的语法树

    构建类型结构的工作主要由图4.4.1第21行调用的CheckDeclarationSpecifiers,第25行调用的CheckDeclarator和第29行调用的DeriveType函数来完成。再次强调,阅读相关代码时,一定要结合语法图来进行。接下来,让我们先分析一下函数CheckDeclarationSpecifiers。当提到声明说明符时,我们应条件反射般地想到”声明说明符形如static const int”,其中static被称为存储类说明符StorageClassSpecifier,而const被称为类型限定符TypeQualifier,int被称为类型说明符TypeSpecifier。函数CheckDeclarationSpecifiers的主要代码如图4.4.2所示。


图4.4.2 CheckDeclarationSpecifiers()

    图4.4.2第8至15行用来检查存储类说明符,按C标准,一个声明中最多只能有一个StorageClassSpecifier,即只能是extern、auto、static或register中的一个。第16至21行用来检查类型限定符const或volatile。第22至48行则用于对int、double、struct和enum等类型说明符进行检查,我们只给出了主要的代码,省略了一些细节。第27行调用CheckStructOrUnionSpecifier函数,由结构体struct和联合体union对应的语法树来构建类型结构;第30行则调用CheckEnumSpecifier函数来为枚举类型构建类型结构;第32行用于处理用户通过typedefint  INT32定义的类型名INT32,第33行查符号表,从符号表中取出类型名INT32对应的类型信息,对typedef intINT32的语义检查会由declchk.c中的CheckTypedef函数完成;第36至46行为int和double等基本类型构造类型结构,我们只在第40行给出了int作为示范。第50行调用Qualify的函数,把从类型说明符TypeSpecifiers中得到的类型信息ty与类型限定符TypeQualifier结合,从而得到与声明说明符对应的类型结构,存到specs->ty中。其中涉及的函数CheckStructOrUnionSpecifier、CheckEnumSpecifier和CheckTypedef相当复杂,我们会在稍后进行讨论。

    在声明中,另一部分的类型信息由声明符Declarator来指定。在图3.3.4和图3.3.7的语法树中,对于a2、a3和fn来说,经语法分析后,实际上存到这3条链表,为讨论方便,我们不妨称这些链表为Declarator链表。

         astPointerDeclarator(对应*) --->  astDeclarator(对应a2);

         astArrayDeclarator(对应[])  --->  astDeclarator(对应a3);

         astPointerDeclarator(对应*) ---> astFunctionDeclarator(对应())---> astDeclarator(对应fn);

    而函数CheckDeclarator要做的事情就是遍历如上所示的某条链表,收集Declarator链表上各结点的类型信息,最终再通过DeriveType函数,综合由CheckDeclarationSpecifiers和CheckDeclarator这两个函数得来的类型信息,为标识符a2、a3和fn构造出完整的类型结构。为了描述图3.3.4和图3.3.7 astPointerDeclarator结点、astArrayDeclarator结点和astFunctionDeclarator结点中的类型信息,图4.4.3第18至26行中引入了结构体struct typeDerivList。第19行的ctor用于描述类型构造符(即指针、数组和函数),第21行的len用于记录数组的长度(例如int arr[3]中的3),第22行的qual用于记录指针的限定符(例如” int * const  ptr”中的const),第23行的sig用于记录函数形参列表的类型。


图4.4.3 CheckDeclarator()

    函数CheckDeclarator的代码如图4.4.3第1至17行所示,第5至7行用于处理当前结点为数组的情况,第8至10行处理当前结点为函数的情况,第11至13行处理当前结点为指针的情况,而第3至4行则对应如上所示的标识符a2、a3和fn。第27行的CheckArrayDeclarator函数完成了对数组结点的类型信息收集,其结果存于第37行创建的struct  typeDerivList对象中,第38行记录类型构造符为ARRAY_OF(即数组),第39行记录数组的长度,而第41行则指向下一个declarator结点的类型信息,最终构成一个由若干个struct  typeDerivList对象构成的链表。第42行则在当前数组结点astArrayDeclarator中记录标识符的名称。第29行递归地调用CheckDeclarator完成了对Declarator链表的逆序遍历。由于C语言在声明数组时,要求数组大小为常数,第30至35行完成了这个检查。

    图4.4.3第44行的函数CheckPointerDeclarator用于收集Declarator链表中的指针结点所包含的类型信息。例如,对于” int * const ptr;”来说,除了指针*外,我们还要把类型限定符const也包含进来,第49至52完成了对类型限定符的处理,第53至57行则创建struct  typeDerivList对象用来存放类型构造符POINTER_TO和类型限定符,并指向链表中下一个结点收集来的类型信息。由于在C文法的声明说明符中,并未对类型限定符const和类型说明符int的顺序进行限制,即const int和int const是等效的,因此,以下ptr1和ptr2的类型是一样的。类型限定符const是修饰指针本身还是修饰其所指向的对象,要看const是位于运算符*的左侧还是右侧。

         const int * ptr1; // ptr1指向一个const int,但ptr1不是const

         int const * ptr2;// ptr2 指向一个 const int,但ptr2不是const

         const int * const ptr3; // ptr3指向一个const int,且ptr3为const

         int * const ptr4;  // ptr4指向一个int,且ptr4为const

    而图4.4.3第9行调用的CheckFunctionDeclarator函数,则从astFunctionDeclarator结点中收集函数形参列表的类型信息。与之相关的代码,如图4.4.4所示。图4.4.4第7行创建了一个struct  signature对象用于保存函数的形参类型信息,第11行创建了一个向量对象用于保存各形参,最终我们为函数构造的类型结构可参见第2章的“图2.4.9 函数的类型结构”。对于新式风格的函数我们会在第13行通过调用CheckParameterTypeList函数获得其形参类型信息。对于第15行注释中所示的旧式风格函数定义f,此处,我们仅处理(a,b,c)这部分,而对于其后的“int a,b;double c;”我们会在分析CheckFunction函数时再处理,因此在第18行我们只是把各个标识符(例如第15行的a,b,c),通过第18行的AddParameter函数加入到向量funcDec->sig->params中,参数的类型信息我们在第19行中先设为NULL。按C标准的规定,形如“void f(a,b,c);”的旧式声明是非法的,我们会在第21至25行对此进行检查。我们在第26行创建了一个struct typeDerivList对象,用来保存从astFunctionDeclarator结点中收集来的类型信息,第27行设置类型构造符为FUNCTION_RETURN(即函数类型),第28行用于保存形参的类型信息。


图4.4.4 CheckFunctionDeclarator()

     对于以下代码而言,对于f(int a,int b)和g(int c, int d)形参列表的类型收集工作都是由函数CheckFunctionDeclarator完成。处理形参列表时,我们实际上就进入了一个新的作用域,即要调用前面章节中我们介绍过的EnterScope函数来创建新的符号表,但对于函数定义g而言,其局部变量e和形参d位于同一个作用域,相关符号信息存放在相同的符号表中。而对于如下所示的函数h而言,其返回值是一个指向void (void)函数的指针,此时Declarator链表上就会出现两个astFunctionDeclarator结点,分别对应(int a1,int a2)和(void),其中(int a1,int a2)处于一个作用域,而(void)又位于另一个不同的作用域。由于h函数体中的a3和形参a2又处在同一个作用域。因此,当我们调用CheckFunctionDeclarator函数分析完(int a1,int a2)时,要先保存当前作用域的符号表,之后我们会在CheckFunction()中检查h的函数体时,再恢复先前保存的符号表,这样就可以使a1、a2和a3处在相同的符号表中。图4.4.4第49至52行的SaveParameterListTable函数实现了保存当前符号表的功能,而第53至60行的RestoreParameterListTable函数则用于恢复符号表。当我们开始处理形参列表时,我们会在图4.4.4第6行调用EnterParameterList函数,为新的作用域创建相应的符号表,实际的工作由第43行的EnterScope完成;当我们检查完形参列表时,在第34行调用LeaveParemeterList函数,实际的工作由第47行的ExitScope函数完成。对于在形参列表中出现的结构体定义,由于其在外围作用域中是不可见的,所以GCC、Clang和UCC编译器会给出相应警告,例如以下函数k中的struct A。第38行的IsInParameterList函数用来判断我们是否处在形参列表中,以便进行报警。而只有位于函数定义中的形参列表,我们才会调用SaveParameterListTable函数来保存其符号表,以便在检查函数体时进行恢复,如图4.4.4第31至33行所示。

         void f(int a,int b);

         void g(int c, int d){

                   int e;

         }

         void (*h(int a1,int a2))(void){

                   int a3;

                   return NULL;

}

         void k(struct A{int a;} b);

    在图4.4.4第13行中,我们调用了CheckParameterTypeList函数来获取形如(inta,int b)的参数列表中的类型信息,相关代码如图4.4.5第1至13行所示。结合图3.3.7的语法树,我们可以发现第8至11行的while循环实现了对各个形参的检查,第9行调用的函数CheckParameterDeclaration完成了实际的形参检查工作。对于形如int a的形式参数,其类型信息也是分散在声明说明符DeclarationSpecifiers和声明符Declarator中,因此其类型信息收集工作也是由第19、20和27行调用的3个函数来完成。在第21至26行的注释中,我们列出了一些错误的形参声明,为节约篇幅,与此相关的代码我们没有给出。


图4.4.5 CheckParameterTypeList()

    对于形参中的数组和函数类型,按C标准的规定,我们需要将其调整为指针类型,第30行调用的函数AdjustParameter()完成了这个工作,与其相关的代码如图4.4.5第54至57行所示。在C的函数声明的形参中,如果出现不完整的类型IncompleteType,C编译器会给出一个警告,例如以下的函数声明f中的struct Data;而在函数定义的形参列表中出现的IncompleteType,则被视为错误,例如以下的函数定义g中的struct Data。其原因是,在不知道struct Data的类型结构时,我们无法为形参b分配栈空间,而对于函数声明f中的形参a,我们并不需要为之分配栈空间。由于类似的原因,C函数声明中的形参名可以省略,但是函数定义中出现的形参名则不能省略。图4.4.5第36至46行完成了对这两种情况的语义检查,第47行则把形参的类型信息加入到向量funcDec->sig->params中。

    void f(struct Data a);

void g(struct Data b){//此时struct Data还未知,视为IncompleteType

}

struct Data{

         intd;

};

     在图4.4.5第37行,我们调用IsIncompleteType函数用来判断某个类型是否完整,与其

相关的代码在type.c中,如图4.4.6所示。图4.4.6第1至5行的代码用于判断数组类型是否完整,第6至10行用于判断枚举类型是否完整,第11至15行用于检查结构体和联合体类型是否完整。在还未见到结构体、枚举、联合体定义时,我们视这些类型为不完整的,第9行和第14行的标志位complete用于做此标记;而数组大小为0时,我们就将其当作不完整类型的。第16行的IsIncompleteType函数实际上是分别调用第1至15行的这3个函数来判断。


图4.4.6 IsIncompleteType()

    至此,我们讨论了CheckDeclarationSpecifiers和CheckDeclarator这两个函数,为了对经两个函数检查后的结果有个感性认识,我们举以下例子说明。对于arr1和arr2而言,其声明说明符都为int,经CheckDeclarationSpecifiers处理后,我们会得到声明中的部分类型信息T(INT)。

         int * arr1[5];

         int (*arr2)[5];

    而对于*arr1[5]和(*arr2)[5]来说,图4.4.7给出了语法分析后、及经过CheckDeclarator函数所做的语义检查后的类型信息链表。结合声明说明符中所得的类型T(INT),遍历语义检查后的链表,从左向右依次把类型构造符POINTER_TO或ARRAY_OF作用在已有类型上,我们可得到arr1是 array[5] of pointer to int,即arr1是一个长度为5的数组,其数组元素的类型为int *。而arr2是pointer to array[5] of int,即arr2是一个指针,指向int[5]类型的数组。我们在“第1.3节 从文法到分析器”一节中实际上举过类似的例子。


图4.4.7 类型信息链表

    结合CheckDeclarationSpecifiers函数所得到的类型信息,遍历如图4.4.7中所示的由若干个structTypeDerivList对象构成的链表,我们就可构建起“第2.4节 C语言的类型系统”中介绍的类型结构,与此相关的代码如图4.4.8所示。第3至4行用于处理指针类型;C语言中,不允许出现函数的数组,也不允许长度为负数的数组,第5至17行对这些情况进行检查,第18行调用ArrayOf()来构建数组的类型结构;而函数的返回值不可以是数组,也不可以函数类型,第20至27行对此进行检查,第28行调用FuntionReturn()来创建函数的类型结构。


图4.4.8 DeriveType()

    经过DeriveType()函数的类型推导,我们会为int* arr1[5]和int (*arr2)[5]这两个声明分别构建如图4.4.9所示的类型结构。在这小节中,我们重点介绍了CheckDeclarationSpecifiers、CheckDeclarator和DeriveType这3个函数,UCC编译器用这3个函数构建起了类型系统。为了形成整体的概念,在讨论CheckDeclarationSpecifiers函数时,我们有意忽略了为结构体构建类型信息的函数CheckStructOrUnionSpecifier。在下一小节中,我们会对此进行分析。


图4.4.9 类型结构示意图


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值