C编译器剖析_4.4 语义检查_外部声明_结构体和数组的初始化
4.4.3 结构体和数组的初始化
在这一小节中,我们来讨论一下声明中的初始化。在不少程序设计语言的教材中,对变量的声明和定义是有严格区分的。但从C标准文法的角度来看,它们都是由非终结符Declaration产生的,因此对变量初始化的语义检查也在declchk.c中进行。我们先举一个例子来说明“我们在这一小节所处的起点和终点”,如图4.4.15所示。在图的左侧,我们给出初始化值{{20},30}经语法分析后形成的语法树,这是我们本小节的起点;在图的右侧,由struct initData对象构成的链表则是我们对左侧语法树进行语义检查后构造的,dt.a在结构体对象dt中的偏移为0,而dt.c的偏移为8,因此图中offset为0的结点代表“我们需要把dt.a初始化为20”,而offset为8的结点代表“我们需要把dt.c初始化为30”,图中右侧的链表就是我们在本小节的终点。
图4.4.15 struct initData
结构体中允许存在位域成员,其初始化就需要经过一些位运算,如图4.4.16第1至第8行注释中的代码所示,对于第7行的pte而言,其实际所占的内存为4字节,我们需要先把初值3左移12位后,再跟初值64进行或运算,然后用所得结果((3<<12)|64)来初始化pte对象。图4.4.16第27至45行的PlaceBitField函数实现了左移运算,对于常量,在编译时我们就可在第31至34行进行常量折叠,而对于不是常量的操作数,我们才需要在第35至43行生成一个进行左移运算的语法树结点。而图4.4.16第9至26行的BORBitField函数实现了按位或运算。
图4.4.16 BORBitField()
接下来,我们来看一下对初值进行语义检查的函数CheckInitializer,如图4.4.17所示。图4.1.17第8至第18行的代码,用于处理形如第9行注释所示的数组初始化,“用字符串来初始化一维数组”是数组初始化的一个特例。图4.4.17第10行调用的CheckCharArrayInit函数用来处理一些非法的情况。 函数CheckCharArrayInit的代码相对简单,我们就不再展开,只在图4.4.17第12至14行注释中给出一些错误示例。而对于结构体或联合体对象而言,其初值也可以是同类型的其他对象,如图第22至23行所示,第26行的CanAssign函数用于判断形如第23行的dt2和dt两者的类型是否匹配,我们已在前面的章节中分析过这个函数,第29至32行则生成一个struct initData对象来记录这个初值。
图4.4.17 CheckInitializer()
图4.4.17第35行调用的CheckInitializerInternal函数才是真正对初值进行语义检查的核心函数,此函数返回后,我们会得到如图4.4.15所示的由若干个struct initData对象构成的链表,对于如图4.4.16第7行所示的初值,我们需要进行按位或运算,从而得到初值((3<<12)|64),图4.4.17第41至53行的代码用于对此进行处理,我们在第46行调用函数BORBitFiled进行了或运算,之后在第47行删除了链表中多余的structinitData对象。
我们需要结合图4.4.15所示的语法树,来分析函数CheckInitializerInternal,与其相关的代码如图4.4.18所示。图4.4.18第7至38用于对整型、浮点型和指针类型等标量类型的初始化进行检查,第39至55行则用于对联合体对象的初始化进行语义检查;而在第56至58行,限于篇幅,我们暂时省略了用于处理数组和结构体类型的代码。图4.4.18第7行判断被初始化对象的类型是否为标量类型,第9至13行的while循环用于处理形如{{3}}的标量初始化值,此时我们给出一个” warning:braces around scalar initializer”警告,表示此处大括号是多余的。第15行调用CanAssign来判断一下能否把初值赋值给被初始化对象,第28行进行调用Cast进行必要的类型转换,但对于第20至22行注释中所示的整型与指针类型之间的初始化代码,由于在32位机器中,指针类型和int型占用相同大小的存储空间,因此此时我们不必构造进行类型转换的语法树结点,第25行的if语句会对此进行判断。第31至36行则创建一个struct initData对象,并将其加入由structinitData对象构成的链表的末尾处。
图4.4.18 CheckInitializerInternal()
而对于联合体对象的初始化,按C标准的规定,我们只对联合体中的最先出现的成员进行初始化,由第41行可获得该成员的类型。第40行的作用是“去掉初值中可能存在的大括号”,第42行递归地调用CheckInitializerInternal函数来进行语义检查。对于图4.4.18第46至49行所示的初值来说,其中的2.0是多余的,此时我们在第51行给出一个警告。对于如下所示的联合体初始化而言,在图4.4.18第42行递归调用的CheckInitializerInternal函数中,我们需要对字符数组char buf[16]进行初始化。
union {
charbuf[16];
intnum;
} dt = {"Hello World.\n"};
接下来,我们来看一下对数组初始化进行语义检查的相关代码,如图4.4.19所示。对于形如第4行注释所示的“用字符串来初始化一维数组”的情况,我们在第5至33行进行特殊处理,完成处理后直接从第33行返回。对于其他形式的数组初始化,我们则在第35至42行的while循环中,递归地调用CheckInitializerInternal函数来对每个数组元素进行初始化。图4.4.19第43至50行的代码,主要是用于处理形如第44行注释所示的未指定大小的数组,在检查完初值{1,2,3,4,5}后,我们可以由初值计算出其数组所占内存的大小。而对于如图第52行注释中所示的数组初始化,其中的4是多余的,我们会在第53行给出一个警告。
图4.4.19 CheckInitializerInternal_ARRAY
图4.4.19第5行的if语句主要是用于判断对字符数组的初始化是否合法,C语言的字符串可以是”abc”,也可以宽字符L”abc”,我们不可以用”abc”来初始化宽字符数组,也不可以用L”abc”来初始化char字符数组,第7至9行的条件对此进行限制。而第5至6行的条件则要求用于初始化数组的字符串应形如”abc”或{“abc”},但不可以是{“abc”, 123}。如果C程序员没有指定字符数组的大小,如图4.4.19第11行注释所示,则我们在第15行计算出其数组元素的个数。如果C程序员指定的数组元素个数正好和字符串长度一样,我们就舍弃字符串末尾的’\0’字符,第18至21行对此进行处理。对于形如char str2[3]= “123456”的数组容量不够的情况,我们在第24行舍弃多余的字符,同时在第25行给出一个警告。第27至32行则创建一个struct initData对象,并插入到链表的末尾。
对结构体进行初始化的代码如图4.4.20所示,对结构体中的各个成员域,我们在第19行递归地调用CheckInitializerInternal函数来处理,如果是位域成员,我们需要在第22行调用PlaceBitFiled函数进行必要的左移运算,例如图4.4.16第6行的(3<<12)。对于形如int arr[3]= {1,2,3,4,5}的初始化,由于4和5是多余的,我们会在第30行给出一个警告。
图4.4.20 CheckInitializerInternal_STRUCT
按C标准的规定,用于初始化全局变量或静态变量的初值应是常量,这样的好处是在C程序开始运行时,我们只需要从外存(或者从ROM)中加载初值映像到内存即可,不需要执行额外的代码来计算初值。C++语言放宽了这个限制,在C++中以下代码是合法的,即我们可以在运行时调用一个函数f()来计算全局变量number的初值,这也意味着函数f()会比main()函数更早执行。但在C语言中,以下代码是非法的,其原因就在于C标准要求全局变量的初值为常量。
int f(void){
//….
}
int number = f();
但对于以下函数g中的局部变量local来说,由于其所处栈空间是在运行时动态分配的,我们可以调用函数f()来对local进行初始化。这在C和C++中都是合法的。
void g(void){
int local = f();
}
由于有这样语义上的差别,在调用函数CheckInitializer完成对初值的检查后,对于全局变量和静态变量,我们还需要再检查一下其初值是否为常量,图4.4.21中的函数CheckInitConstant用于此目的。图4.4.21第3行的while循环用于遍历由若干个structinitData对象构成的链表,第4行的if条件用于检查初值是否为常量,这包括op域为OP_CONST的语法树结点(例如整型或浮点型常数)、字符串常量OP_STR和地址常量。对于地址常量的检查由第14行的函数CheckAddressConstant来完成。地址常量的类型应是指针类型,第18行对此进行判断。由于C语言存在指针运算,对于形如ptr+n或ptr-n的指针运算,如果ptr是地址常量,且n为常量,则ptr+n和ptr-n是地址常量,这个规则可递归地应用到表达式(ptr+n)+k上,我们最终可得到常量ptr +(n+k),第20至28行的if语句用于对此进行处理。
图4.4.21 CheckInitConstant
我们可用以下全局变量的初始化来说明图4.4.21第29至60行的代码。可以发现,在汇编代码中,符号number和arr实际上对应一个地址常量。在C语言中,&number的类型是指针类型,但在汇编指令中,我们使用符号number即可表示一个地址常量,图4.4.21第29至33行对此时行预处理。第34至45行用于处理形如arr[2]或a.b.c这样的地址常量,它们的共同特征是最终化简为形如addr+k的形式,其中addr为类似number或arr的地址常量,而k为把各偏移进行累加所得到的常量。对于对下arr[2]来说,按数组的寻址方式,arr[2]对应的地址为arr+2*5*sizeof(int),即arr+40。第50至59行的代码用于为addr+k构造一个进行addr和k之间加法运算的语法树结点。第47行的if条件要求addr是标识符ID,在此前提下,addr可以是数组名或者函数名,当然若addr为标识符ID且“第14行的形参expr对应的表达式形如&number这样的取地址运算”也是可以的。
int number;
int arr[4][5];
int * ptr1 = &number;
int * ptr2 = arr[2];
////////////对应的汇编//////////////
ptr1: .long number
ptr2: .long arr + 40
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。