C语言的内存管理
1、内存管理的概述
当程序被加载到内存的时候,它在内存中会大致被组织成三个部分:代码区,静态存储区和动态存储区。代码区存放的是将要执行的程序的机器语言表示,包括组成程序的各种用户自定义函数和系统调用函数。关于静态存储区和动态存储区:The word static refers to things that happen at compile time and link time when the program is constructed—as opposed to load time or run time when the program is actually started; The term dynamic refers to things that take place when a program is loaded and executed. 就是说静态存储区和动态存储区的主要的区别在于空间分配的时间不同,静态存储区的空间实在程序编译和连接的时候分配的,而动态存储区分配的空间是在程序调入和执行的时候分配的。
静态存储区中主要存放全局的(global),静态的(static)数据。其可以分为两部分,一部分用来存放已经初始化的数据,另一部分用来存放没有初始化的数据(这部分数据会被默认初始化为0,这块区域称为BSS(Block Started by Symbol)段,是用来存放程序中未初始化的全局变量的一块内存区域。)。动态存储区分为两部分,即堆(heap)和栈(stack)。堆中主要存放在程序运行过程中调用calloc和malloc函数动态分配的内存空间,当在程序中调用calloc和malloc函数来为程序分配新的空间时,堆空间便会向上增长,如图1。栈用来存放局部变量,用来传递函数的参数,并用来记录函数的返回地址(当函数执行结束,根据改地址程序可以跳转到相应的指令出继续执行)。当一个新的函数被调用,会有一个新的栈frame被加入到占空间当中,这时栈向下增长,如图1。

通常来说,堆和栈在进程的虚拟地址空间的两端(从上面的图上也可以看出来)。当被访问的时候,栈地址空间就会增长,最大可以到一个由内核设定的值(可以通过setrlimit(RLIMIT_STACK,...)进行调整)。而堆空间的增长则是由于内存分配函数(calloc和malloc)触发了brk()和sbrk()系统调用,这样会使操作系统将更多的物理内存页面映射到该进程的虚拟地址空间。
可见,堆空间和栈空间的管理是由操作系统来实现的。通常,游戏等对性能要求较高的应用会有自己的内存管理方案。(比如,一次从堆中申请大块的空间,然后内部分配,这样就避免了依赖于操作系统的内存管理。)
2、栈
2.1 介绍
在计算机体系结构中,栈是一个后进先出的数据结构。在大多数现在计算机系统中,每一个线程多会有一个保留的内存区域作为栈来使用。当函数执行的时候,它会将一些状态信息放在栈顶;当函数退出的时候,它负责将数据从栈中移除。栈的一个重要用途就是用来保存函数调用的地址,来保证return语句能够返回到正确的位置。(当然,还有其他用途。)
因为栈中的数据时后进先出的,这种方式比较简单。因此,通常栈的内存分配要比堆的内存分配(也就是常说的动态内存分配)要快。另外一个特性是,当函数退出时,它在栈中占用的内存空间将会被自动释放。如果其中的数据是不需要的,这对于编程人员来说是非常方便的。当然,如果有些数据还需要被保留,那么这些数据一定要在函数退出之前被拷贝到其他地方。因此,基于栈的内存分配,适合于存放临时数据,或者是函数退出之后不再需要的数据。
一个栈是由一些栈frame组成的。这是一种和机器相关的保存着子程序状态信息的数据结构。每一个栈frame对应着一个还没有被return语句终止的子程序。例如,一个名为DrawSquare的程序调用了一个名为DrawLine的子程序,栈顶的格局便如下:

栈比较快的原因是,它的访问方式比较简单,这样比较容易从中分配内存,而堆有着复杂得多的内存分配和释放机制。另外一方面,栈中的每一个字节都会被经常重复使用,这样,在实现上,它们可能会被映射到处理器的cache,这也是栈计较快的原因。
2.2 tip
下面是关于栈的一些tip:
(1)在栈中创建的变量在超出作用域后便会被自动释放。
(2)栈中分配的变量要比堆中要快的多。
(3)栈是用一个真正的栈数据结构实现的,用来存储局部变量,函数的返回地址,并用来参数传递。
(4)如果超量使用会导致栈溢出(比如无限递归,超大的分配等)。
(5)栈中的数据不用指针就可以访问。
(6)如果在运行之前,你知道需要多大的空间来存放数据,而且这个空间不会导致溢出,那么这时候应该使用栈。
(7)通常,在程序开始的时候,一个栈空间的最大值就已经被确定了。
(8)在C语言中,可以是用alloca实现变量长度的内存分配,这会在栈上分配空间,而不会想alloc一样在堆上分配空间。尽管这样的内存空间在return之后也会被释放,但是用来做buffer还是很有用的。
2.3 栈溢出
栈相关的内存错误是最糟糕的。如果使用堆内存分配,在越界时,可能会触发一个segment fault(当然,也不是所有时候都会这样,比如恰好两个分配的空间时连续的。)。但是,由于在栈上创建的变量相互之间都是连续的,如果在写的时候越界会改变另一个变量的值。现在,我感觉只要是我的程序不按逻辑运行了,我就知道很可能是缓冲区溢出了。
下面是一些毁掉栈的例子:
(1)利用下面的方法,我们可以用掉比该线程可用的栈空间更要多的内存空间,从而导致栈溢出。
(3)向已经释放的栈空间中写入数据
第二次调用foo后:
非常清晰。
#include <stdlib.h>void f(void){int* x=malloc(10*sizeof(int));x[10]=0;//problem 1:heap block overrun; problem 2:memory leak, x not freed.}int main(void){f();return 0;}终于完了,基本上是把原文翻了一遍。(废我一下午的说)
原文《Memory management in C: The heap and the stack》Leo Ferres, Department of Computer Science, Unverisidad de Concepcion, leo@udec.cl, October 7,2010