C语言解释器Java版-1-内存分配
2015-11-29 15:48
274 查看
1 内存分配
基本了解了内存模型后,接下来考虑在实现解释器时如何进行内存分配,即怎样将每个内存区进行数据分配和释放。在进行具体内存分配前,需要定义输入接口:被解释的源程序 file.c 和入口函数 func,然后根据输入生成对应的AST、控制流程图和符号表。有了这些原始数据后,开始进行内存分配。
1.1 全局数据分配
这里的全局数据包括如下几类,每一类的分配都不尽相同。主要操作的内存区是代码段区和数据区。1.1.1 函数内存分配
被解释程序file.c中的所有已定义函数,不包含extern的外部函数和 没有声明定义的函数。函数对应的内存段区是代码段区。在标准的C语言内存分配中,代码段区存放的是函数对应的二进制代码。在我们的实现中,每一个函数在代码段区分配一个四字节的内存空间,内存地址递增。由于每个函数占用的内存大小相同,都是4个字节,所以这里不需要考虑内存对齐问题。(代码段区起始地址应该能被4整除)
1.1.2 字符串常量
在C语言中,字符串常量一般是一个char*的定义:在分配内存时无法从符号表中通过符号类型获取长度;
为了统一处理指针类型(使用四个字节地址表示),char*被分配为一个四字节的内存地址,char*的值则分配到一块固定内存范围(即字符串常量区);
这样分配的另一个好处是,对于同样的字符串,只需要从指定的内存地址获取字符串的值即可,不需要另外开辟一段空间去存放字符串的值,能够节省空间。
另外,字符串常量区不需要考虑内存对齐,每一个字符都是一个char,所有不用考虑内存对齐问题。
1.1.3 全局定义变量
包含初始化和未初始化的变量。虽然在标准的定义中,区分了初始化和未初始化,但是,为了实现简便,我们的实现中做了合并处理。对于每一个非指针类型的全局变量,按照大小分配指定的空间即可;
对于类型为指针的全局变量,需要分配的是4字节的地址空间,对于指向的内容,会在使用的时候进行指向处理。
每个全局变量占用的内存并不相同,所以,在进行内存分配时,需要考虑内存对齐。但是,在查阅相关文档时,并未获得有关全局变量在分配时如何进行内存对齐处理。所以,我们采用的方案是:分配的内存起始地址满足当前变量的对齐要求即可。
1.1.4 静态变量
包含全局定义和局部定义的静态变量。静态变量的地址分配和全局变量没有区别;
在全局数据分配时,需要分配局部静态变量,所以需要遍历每一个函数,并找到该函数中定义的局部静态变量,并对其分配内存;
局部静态变量是需要关注初始化问题的。对于一个局部静态变量,第一次调用变量所属函数和第二次调用的值是不同的。有如下代码,第一次调用时sv的值是1,第二次调用时sv的值就是2….所以在第二次调用时,就需要注意,sv的赋值语句已经不起作用了。
void f(){ static int sv = 1; printf("%d",sv); sv++; }
1.2 局部数据分配
局部数据分配,主要操作的内存区是栈区和堆区。在进入函数时,需要立即进行局部变量的内存分配。分配的局部变量包括:函数形参、每一个作用域的局部变量。
内存分配原则是,先参数,后变量,参数从右向左,变量按定义顺序。如下所示代码,分配内存的顺序是:b->a->c1->d1->c2->d2。
需要注意,在一进入函数f后,就需要将c2和d2的内存进行分配。这样对于永远不可能走到的分支,可能会无效分配很多内存,但是这是VC、GCC等编译器的共同选择。这样做可以解决的一个问题是,函数中的跳转语句或者其他循环语句,只分配一次内存,且可以解决因为这些语句引起的找不到内存、内存未分配等问题。
int f(int a,int b){ int c1,d1; //.... if(c1 + c2 > 0){ int c2,d2; //.... } return 1; }
进入函数时分配内存,意味着,退出函数时,需要释放内存。由于这些内存都是在栈中分配的,所以进入函数时,需要创建一个新的栈空间,存放函数中的局部变量,退出函数时,退出该栈空间(栈帧,stackFrame)即可。
栈区的内存分配是从高地址向地址进行分配的,也就是b比a的地址高。但是要注意,从高到低分配并不意味着起始地址就低。对于变量b,如果起始地址是0x70000004,则b的最大地址位置是0x70000008,而不是0x70000000,a的起始地址是0x70000000,最大地址是0x70000004。
堆区的内存分配是从低地址向高地址进行的。但是堆区的地址分配释放策略有很多种,不同的策略会导致分配的地址差异。对于堆内存的释放和分配策略,这里不多做说明,如果后续有需要,会单独进行说明。
1.3 函数调用
在解释执行函数调用语句时,需要先保存当前栈空间,然后切换到新的函数,创建新的栈空间,当新函数执行完成后,退出新的栈空间,进行栈还原,并执行下一条语句。这里,将每一个函数的一个栈空间称为一个栈帧(stackFrame)。函数调用的栈帧组成如下图所示:每个frame由局部变量和参数、返回地址构成。在图中,main函数调用f1函数时,main的stackframe中保存了f1函数的实参,而f1函数的stackFrame中保存了main函数下一条语句的地址,也就是f1函数的返回地址。
当main函数开始调用f1时,先将调用的实参放入到mainframe,创建新的f1 stackFrame,将main函数调用完成f1后的下一条语句地址放入f1 stackFrame中。
f1 stackFrame的其他创建工作还包括局部变量的分配等。在实际解释执行时,内存分配的同时,需要对参数进行赋值。这时候会读取mainframe中的实参值对f1的形参赋值。需要注意,这里的赋值也可能发生隐式类型转化。
函数调用的另外一个问题是可变参数的问题。在我们目前实现中,对于可变参数是直接忽略掉了。如果后续有需求,对可变参数会重新进行说明。
相关文章推荐
- c++11中的右值引用以及移动构造
- 【C语言】递归实现栈的逆序及排序
- for循环 数组下标越界导致死循环
- c++11中的using关键字
- 编译原理(七) 算符优先分析法(构造算符优先关系表算法及C++实现)
- c++11中decltype的妙用
- bitset应用---产生1万个不重复的随机数
- c++11中的智能指针
- c++模板实现多参数任意传 - 类实现
- effective c++ 笔记之static_cast
- vs2008 dlib编译问题USER_ERROR__missing_dlib_all_source_cpp_file__OR__inconsistent_use_of_DEBUG_or_ENABLE
- 【C++】满二叉树与完全二叉树的区别及判断
- C++引用方式实现两个值的互换
- C++运算符重载
- C++抛出异常与传递参数的区别
- C++抛出异常与传递参数的区别
- C++【线程同步】-临界区同步
- c/c++进制转换
- C++的chrono、ratio和ctime等头文件
- leetcode Search a 2D Matrix