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

来源:互联网 发布:电棍在淘宝上暗语 编辑:程序博客网 时间:2024/04/29 11:56

    在这一小节中,我们将对形如第3章图3.3.17所示的结构体语法树进行语义检查,从而构建结构体的类型结构。

        

图3.3.17 ParseStructOrUnionSpecifier()构建的语法树

    我们在第2章中给出了以下结构体struct Data对应的类型结构,如图2.4.4所示。为了阅读方便,我们重新给出这2幅图,由图示我们也能较清楚地预览本节的起点和终点。

struct Data{

         int abc:8;

         int def:24;

         double f;

} dt;

 

图2.4.4   结构体的类型结构

    按照C的标准文法,根据“结构体名”和“大括号”的有无情况,结构体或联合体说明符StructOrUnionSpecifer可分为以下4种情形,其中第4种情况已在语法分析时报错。

         (1) struct Data1       // 有结构体名但无“大括号”

         (2) struct {int a; int b;} // 无结构体名但有“大括号”

         (3) struct Data2{int a; int b;} // 有结构体名也有“大括号”

         (4)struct                    // 无结构体名也无“大括号”,语法分析时已报错

    如图4.4.10所示CheckStructOrUnionSpecifier函数的相关代码主要是用于处理以上4种情况,第9至19行用于处理形如struct Data1的“有名无大括号”的情况,第9行的LookupTag用来查询符号表,如果找不到,则在第12行调用StartRecord创建一个如图2.4.4所示的struct recordType对象,第13行的AddTag函数用于在符号表中添加一项关于struct Data1的符号信息,此时结构体structData1的类型还不是完整的,我们暂时只有一个structrecordType对象,还没有形成如图2.4.4所示的完整类型结构。


图4.4.10 CheckStructOrUnionSpecifier()

    对于“无名但有大括号”的情况,第20至23行会进行预处理,第22行置complete为1,这意味着当CheckStructOrUnionSpecifier函数返回时,我们就能得到一个完整的结构体类型结构。而真正构成如图2.4.4类型结构的工作,则在第52至59行来完成。结构体中的域成员在C标准文法中被称为StructDeclaration,可能取名StructFieldDeclaration会更妥些,第55行调用CheckStructDeclaration来对成员域进行检查,由此获得的成员域类型信息会存放在如图2.4.4所示的struct filed对象中。由第54行的while循环得到各成员域的类型信息后,我们还需要进行综合,从而计算出各成员域在整个结构体中的偏移offset信息,这些信息最终决定了结构体对象在内存中的布局,第58行的EndRecord函数完成了这个工作。第24至46行的代码用于对“有名且有大括号”的情况进行预处理。

    简而言之,构建如图2.4.4类型结构的主要步骤有:

(1) 调用StartRecord函数构建一个structrecordType对象;

(2) 为各个成员域调用CheckStructDeclaration函数来创建相应的structfiled对象;

(3) 调用EndRecord函数来计算结构体的内存布局,主要是各成员域的偏移位置。

    下面,我们就依次来对相关函数进行讨论,图4.4.11给出了StartRecord函数的相关代

码,第3行在堆空间中创建了一个structrecordType对象,第5至8行对其进行初始化,第7行的rty->flds指向若干个structfiled对象构成的链表,第8行置complete标志位为0,表示这还是一个不完整的结构体类型。通过CheckStructDeclaration函数获得的成员域类型信息会存放在一个struct filed对象中,可调用第11行的AddField函数,把struct filed对象添加到struct recordType对象中。

     

图4.4.11 StartRecord()

    C语言中,允许在结构体的末尾处定义变长的数组,语法上如下struct Packet所示,图4.4.11第17行的hasFlexArray用于记录结构体中是否有变长数组。当结构体中有一个成员域含有const限定符时,则整个结构体对象要被视为const,第21行置标志位hasConstFld为1,第23至28行创建了一个struct filed对象并进行相应的初始化,第29和30行把这个对象加入rty->flds所指向的单链表中。

         struct Packet{

                   int len;

                   char data[];

         };

    图4.4.11第33行的LookupField函数用来检索在structrecordType对象中是否存在名为id的成员域,当成员域没有名字而且其类型也是无名结构体时,此时我们需要在第40行递归调用LookupField函数,在这个“无名结构体”中进行查找,这适合于处理如下所示的dt.b。

         struct Data{

                   struct { // 无名结构体,若改为struct ABC,则导致dt.b语法错误

                            int a;

                            int b;

                   };       // 无名域成员

                   int c;

         }

         struct Data1 dt;

         dt.b;

    需要注意的是,如果把上述“无名结构体”改成有名的,例如改为struct ABC,则由于在C语言中,struct ABC并不被当成struct Data的“内部类”,两者所处作用域是相同的,这会导致dt.b被视为语法错误,此时,C编译器认为在struct Data中并不存在名为b的成员域。

    接下来,我们来看一下对结构体成员域进行检查的函数CheckStructDeclaration,如图4.4.12所示。对于结构体成员域中的“声明说明符”,我们在第28行调用函数CheckDeclarationSpecifiers来获取其中的类型信息,对于形如第33行的多个声明符,我们在第36至39行的while循环调用函数CheckStructDeclarator来处理。


图4.4.12 CheckStructDeclaration()

    函数CheckStructDeclarator的代码如图4.4.12第1至24行所示,在第6行我们调用CheckDeclarator来获取声明符中的类型信息,第8行通过调用DeriveType来对“声明说明符DeclarationSpecifiers”和“声明符Declarator”这两部分的类型信息进行综合,最后通过第23行的AddField函数把成员域的类型信息添加到结构体的类型结构中,从而构成图2.4.4所示的类型结构。在第12至22行的注释中,我们给出了结构体定义的一些错误例子,在函数CheckStructDeclarator中,我们需要对检测这些错误,相关代码并不太复杂,这里从略。由此可以发现,我们最终仍然是按照CheckDeclarationSpecifiers、CheckDeclarator和DeriveType这三部曲,来为结构体成员域构建类型结构。

    由于已经知道了结构体中各成员的类型信息,我们就能知道各成员域所要占的内存大小,而且成员域是依次进行定义的,由此可计算其在结构体对象中的偏移位置,这个工作由EndRecord函数来完成。只是碰到“无名结构体”时,会相对麻烦一些,如图4.4.13第3至第7行代码所示。


图4.4.13 AddOffset()

    在C语言中,对于以下代码而言,当我们使用dt.b3时,我们需要知道b3在整个struct Data中的偏移,由图4.4.13第2行,我们可以发现成员域a的偏移为0,其大小为4,之后的偏移为4,而第6行的b3在无名结构体中的偏移为8,两者相加,则可以得到b3在整个结构体struct Data中的偏移为12。但是对于dt.d.d3来说,我们只要计算出第13行的d在struct Data的偏移,再计算出d3在第9行所对应的无名结构体的偏移即可,在中间代码生成时,我们先处理dt.d,之后再处理(dt.d).d3。

         struct Data dt;

         dt.b3;

         dt.d.d3;

    图4.4.13第16行的AddOffset函数用于为形如第3至第7行的“无名结构体”计算各成员域的偏移,考虑到“无名结构体”内可能还有嵌套的“无名结构体”,我们需要在第21行递归地调用AddOffset来计算相应的偏移。对于图4.4.13第9至13行的“无名结构体”,由于成员域d是有名的,所以我们不需要调用AddOffset来计算d1、d2和d3在整个struct Data中的偏移。

    在此基础上,我们来分析一下EndRecord函数,为了能更清楚地看到整个代码的总体流程,我们在图4.4.14第13至21行忽略了对于结构体位域成员的处理,第6至31行用于处理结构体类型,而第32至46行则用于处理联合体类型。对于联合体而言,我们要遍历形如“图2.4.4 结构体类型结构”中的struct field对象链表,找出其中占最大内存空间的成员,此成员的大小也就是整个联合体对象要占内存的大小。联合体中不可以有变长数据,第42至46行对此进行检查。


图4.4.14 EndRecord()

    而对于结构体而言,整个结构体对象所占内存空间的大小,至少是各成员域所占内存大小的总和,如果考虑到成员域的对齐,则有时还要偏大一点。例如在以下结构体struct Data中,各成员域所占内存大小的总和为5字节,但考虑到int型一般是按4字节来对齐的,所以在成员ch和num会有3字节的空白,对齐后的整个struct Data所占内存大小为8字节。图4.4.14第7至26行的while循环,会对各成员域所占内存大小进行累加,并在第8行得到当前成员域的偏移,在第9行用ALIGN宏来进行对齐。对于结构体内的“无名结构体”,我们在第11行调用前文讨论过的AddOffset函数,从而计算“无名结构体”内的成员域在整个结构体中的偏移。

         struct Data{

                   char ch;    //占1字节

                   int num;  // 占4字节

         };

    而对于如下结构体struct Buffer而言,由于整个结构体structBuffer中只有一个变长数组,这被视为非法的,我们会在第47至51行对此进行检查。

         struct  Buffer{

                   char buf[];

         };

     与图4.4.14第13至21行注释所对应的代码,主要用于处理以下情况:

(1) 如图4.4.14第16行所示,当前成员b1不是位域,此时,前一个位域a1虽然只有12位,但我们让其独占4个字节,即一个int型所占的内存大小;

(2) 如图4.4.14第18行所示,当前成员b2是位域,且能够放置在一个int型剩余的位空间中,例如此处a2占去了12位,还剩20位,足够b2存放;

(3) 如图4.4.14第19行所示,当前成员b3是位域,但一个int型剩余的位空间已经不够存放,a2和b2共占去了24位,剩下的8位不够存放b3。此时我们可以新开辟一个int型来存放b3的20位数据。

    总之,构建类型结构的三部曲CheckDeclarationSpecifiers、CheckDeclarator和DeriveType才是最关键的地方。在此基础上,去理解对枚举enum进行检查的函数CheckEnumSpecifier ,及用于检查typedef自定义类型名的CheckTypedef函数,则相对容易,我们就不再啰嗦。
0 0
原创粉丝点击