step12 实验指导
本实验指导使用的例子为:
int func(int param[]){
param[0] = 1;
return 0;
}
int main() {
int arr[4] = {1,2};
func(arr);
return arr[0] + arr[1] + arr[2];
}
词法语法分析
我们需要增加一个数组的初始化列表,可以直接修改上一节数组的AST结点增加一个数组用于记录初始化元素。
函数的参数列表需要加上数组类型。
语义分析
由于 step 12 里额外引入了数组传参和数组初始化,所以你需要修改语义分析,以支持数组传参。传参出现了一种特殊情况,即:函数参数数组的第一维可以为空。
int fun(int a[][12]){
a[0][1] = 1;
return 0;
}
中间代码生成
在C语言中,对于全局数组,如果没有初始化,那么其值全为0,而对于局部数组来说,如果没有初始化,其值是未定义的。
而初始化后数组的元素值是确定的,如果初始化时指定的的元素个数比数组大小少,剩下的元素都回被初始化为 0。例如:
int arr[3]={1,2};
// 等价于
int arr[3]={1,2,0};
当数组长度较长时,如果对每个位置产生一条赋值语句可能会让生成的汇编代码非常冗长。因此你可能需要内置一个 memset
这样的函数来实现数组的清零。由于gcc的汇编器通常自带一个memset
函数,我们这里采用fill_n
命名。
// fill_n 函数原型,三个参数分别是目标内存地址,设置的内容,长度(以数组元素个数为单位)
int fill_n(int *dst, int res, int cnt);
因此,上述初始化可以等价地转化为:
int arr[3];
fill_n(arr, 0, 3);
a[0] = 1;
a[1] = 2;
目标代码生成
数组传参相对于初始化是简单的,回想函数一节的传参方式,自行实现。
思考题
- 作为函数参数的数组类型第一维可以为空。事实上,在 C/C++ 中即使标明了第一维的大小,类型检查依然会当作第一维是空的情况处理。如何理解这一设计?
总结
恭喜你实现了 MiniDecaf 语言的所有特性。回过头看,我们从常量表达式开始,逐步为编译器增加变量、作用域等特性,又引入控制逻辑,最后实现全局变量和数组,编译器逐渐变得功能齐全。编译器每一个新的特性都带来了新的挑战,而你通过自己的智慧,逐步解决了这些挑战。顺利完成实验后,相信你对编译器也有了自己独特的理解。