天天看点

堆栈、栈帧、函数调用、内存分配总结

http://blog.21ic.com/user1/5585/archives/2009/56681.html

1) 在栈上创建。在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,一般使用寄存器来存取,效率很高,但是分配的内存容量有限。

2) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete来释放内存。动态内存的生存期由程序员自己决定,使用非常灵活。 

3) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。 

4) 文字常量分配在文字常量区,程序结束后由系统释放。 

5)程序代码区。

经典实例:(代码来自网络高手,没有找到原作者)

一 Code  

#i nclude <string> 

int a=0;    //全局初始化区 

char *p1;   //全局未初始化区 

 void main() 

    int b;//栈 

    char s[]="abc";   //栈 

    char *p2;         //栈 

    char *p3="123456";   //123456\0在常量区,p3在栈上。 

    static int c=0;   //全局(静态)初始化区 

    p1 = (char*)malloc(10); 

    p2 = (char*)malloc(20);   //分配得来得10和20字节的区域就在堆区。 

    strcpy(p1,"123456");   //123456\0放在常量区,编译器可能会将它与p3所向"123456\0"优化成一个地方。 

}

二 三种内存对象的比较 

  栈对象的优势是在适当的时候自动生成,又在适当的时候自动销毁,不需要程序员操心;而且栈对象的创建速度一般较堆对象快,因为分配堆对象时,会调用operator new操作,operator new会采用某种内存空间搜索算法,而该搜索过程可能是很费时间的,产生栈对象则没有这么麻烦,它仅仅需要移动栈顶指针就可以了。但是要注意的是,通常栈空间容量比较小,一般是1MB~2MB,所以体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,因为随着递归调用深度的增加,所需的栈空间也会线性增加,当所需栈空间不够时,便会导致栈溢出,这样就会产生运行时错误。 

  堆对象创建和销毁都要由程序员负责,所以,如果处理不好,就会发生内存问题。如果分配了堆对象,却忘记了释放,就会产生内存泄漏;而如 果已释放了对象,却没有将相应的指针置为NULL,该指针就是所谓的“悬挂指针”,再度使用此指针时,就会出现非法访问,严重时就导致程序崩溃。但是高效的使用堆对象也可以大大的提高代码质量。比如,我们需要创建一个大对象,且需要被多个函数所访问,那么这个时候创建一个堆对象无疑是良好的选择,因为我们通过在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享,相比整个对象的传递,大大的降低了对象的拷贝时间。另外,相比于栈空间,堆的容量要大得多。实际上,当物理内存不够时,如果这时还需要生成新的堆对象,通常不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。

  静态存储区。所有的静态对象、全局对象都于静态存储区分配。关于全局对象,是在main()函数执行前就分配好了的。其实,在main()函数中的显示代 码执行之前,会调用一个由编译器生成的_main()函数,而_main()函数会进行所有全局对象的的构造及初始化工作。而在main()函数结束之 前,会调用由编译器生成的exit函数,来释放所有的全局对象。比如下面的代码:

void main(void) 

… …// 显式代码 

}

实际上,被转化成这样:

void main(void) 

_main(); //隐式代码,由编译器产生,用以构造所有全局对象 

… … // 显式代码 

… … 

exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象 

}

  除了全局静态对象,还有局部静态对象通和class的静态成员,局部静态对象是在函数中定义的,就像栈对象一样,只不过,其前面多了个static关键字。局部静态对象的生命期是从其所在函数第一次被调用,更确切地说,是当第一次执行到该静态对象的声明代码时,产生该静态局部对象,直到整个程序结束时,才销毁该对象。class的静态成员的生命周期是该class的第一次调用到程序的结束。

三 函数调用与堆栈

1)编译器一般使用栈来存放函数的参数,局部变量等来实现函数调用。有时候函数有嵌套调用,这个时候栈中会有多个函数的信息,每个函数占用一个连续的区域。一个函数占用的区域被称作帧()。同时栈是线程独立的,每个线程都有自己的栈。例如下面简单的函数调用:

堆栈、栈帧、函数调用、内存分配总结

另外函数堆栈的清理方式决定了当函数调用结束时由调用函数或被调用函数来清理函数帧,在VC中对函数栈的清理方式由两种:

参数传递顺序 谁负责清理参数占用的堆栈
__stdcall 从右到左 被调函数
__cdecl 从右到左 调用者

2) 有了上面的知识为铺垫,我们下面细看一个函数的调用时堆栈的变化:

代码如下:

堆栈、栈帧、函数调用、内存分配总结

Code

int Add(int x, int y)

{

    return x + y;

}

void main()

{

    int *pi = new int(10);

    int *pj = new int(20);

    int result = 0;

    result = Add(*pi,*pj);

    delete pi;

    delete pj;

}

对上面的代码,我们分为四步,当然我们只画出了我们的代码对堆栈的影响,其他的我们假设它们不存在,哈哈!

第一,int *pi = new int(10);   int *pj = new int(20);   int result = 0; 堆栈变化如下:

堆栈、栈帧、函数调用、内存分配总结

第二,Add(*pi,*pj);堆栈如下:

堆栈、栈帧、函数调用、内存分配总结

第三,将Add的结果给result,堆栈如下:

堆栈、栈帧、函数调用、内存分配总结

第四,delete pi;    delete pj; 堆栈如下:

堆栈、栈帧、函数调用、内存分配总结

第五,当main()退出后,堆栈如下,等同于main执行前,哈哈!

堆栈、栈帧、函数调用、内存分配总结

http://blog.csdn.net/zhongguoren666/article/details/7586074

函数调用是程序设计中的重要环节,也是程序员应聘时常被问及的,本文就函数调用的过程进行分析。

一、堆和栈

首先要清楚的是程序对内存的使用分为以下几个区:

l         栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值等。操作方式类似于数据结构中的栈。

l         堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表。

l         全局区(static):全局变量和静态变量存放在此。

l         文字常量区:常量字符串放在此,程序结束后由系统释放。

l         程序代码区:存放函数体的二进制代码。

典型的内存区域分配如图所示:

堆栈、栈帧、函数调用、内存分配总结

其次是堆和栈的申请方式:

栈由系统自动分配,速度较快,在windows下栈是向低地址扩展的数据结构,是一块连续的内存区域,大小是2MB。

堆需要程序员自己申请,并指明大小,速度比较慢。在C中用malloc,C++中用new。另外,堆是向高地址扩展的数据结构,是不连续的内存区域,堆的大小受限于计算机的虚拟内存。因此堆空间获取和使用比较灵活,可用空间较大。

二、栈帧结构和函数调用过程

栈在函数调用中的作用:参数传递、局部变量分配、保存调用的返回地址、保存寄存器以供恢复。

栈帧(stack Frame):一次函数调用包括将数据和控制从代码的一个部分传递到另外一个部分,栈帧与某个过程调用一一映射。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低址地)。

函数调用规则:

l         _cdecl:按从右至左的顺序压参数入栈,由调用者把参数弹出栈。由于每次函数调用都要由编译器产生清楚堆栈的代码,所以使用_cdecl的代码比使用_stdcall的代码要大很多,但是这种方式支持可变参数。对于C函数,名字修饰约定为在函数名前加下划线。对于C++,除非特变使用extern C,C++使用不同的名字修饰方式。

l         _stdcall:按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数。

l         _fastcall:主要特点就是快,因为它是通过寄存器来传送参数的,和__stdcall很象,唯一差别就是头两个参数通过寄存器传送。注意通过寄存器传送的两个参数是从左向右的,即第一个参数进ECX,第2个进EDX,其他参数是从右向左的入stack。返回仍然通过EAX。

最后,以一个例子来解释函数调用过程

void func(int param1 ,int param2,int param3)

{

       int var1 = param1;

       int var2 = param2;

       int var3 = param3;

       printf("param1地址:0X%08X/n",&param1);

       printf("param2地址:0X%08X/n",&param2);

       printf("param3地址:0X%08X/n",&param3);

       printf("var1地址:  0X%08X/n",&var1);

       printf("var2地址:  0X%08X/n",&var2);

       printf("var3地址:  0X%08X/n",&var3);

}

int main(int argc, char* argv[])

{

       func(1,2,3);

       return 0;

}

运行结果如图:

堆栈、栈帧、函数调用、内存分配总结

下面分析调用过程:

在堆栈中变量分布是从高地址到低地址分布,EBP是指向栈底的指针,在过程调用中不变,又称为帧指针。ESP指向栈顶,程序执行时移动,ESP减小分配空间,ESP增大释放空间,ESP又称为栈指针。3个参数以从左向右的顺序压入堆栈,及从param3到param1,栈内分布如下图:

堆栈、栈帧、函数调用、内存分配总结

然后是返回地址入栈:此时的栈内分布如下:

堆栈、栈帧、函数调用、内存分配总结

通过跳转指令进入函数后,函数地址入栈后,EBP入栈,然后把当前ESP的值给EBP,汇编指令如下:

push ebp

mov ebp esp

此时栈顶和栈底指向同一位置,栈内分布如下:

堆栈、栈帧、函数调用、内存分配总结

然后是    int var1 = param1; int var2 = param2; int var3 = param3;按申明顺序依次存储。

堆栈、栈帧、函数调用、内存分配总结

继续阅读