程序的堆和栈及栈帧的组成

 要深入理解函数及其和指针的结合应用,需要理解程序栈.大部分的现代的块结构语言,比如C语言都用到了程序栈来支持函数执行.

调用函数时,会创建函数的栈帧并将其推到程序栈上.函数返回时,其栈帧从程序栈上弹出.

程序帧
       程序栈数支持函数执行内存空间区域,通常和堆共享,也就是说,它们共享一块内存区域.程序栈通常占据这块区域的下部,而堆用的是上部.也就是栈下堆上.
       程序栈存放帧(stack frame),栈帧有时候也称为活跃记录(activation record)或者是活跃帧(activation frame).栈帧存放函数参数和局部变量.堆管理动态内存.

      基于下面这个源码从原理说明栈和堆的结构

void function2()
{
    Object *var1=...;
    int var2;
    printf("Programe Stack Example\n");
}
void funtion1()
{
    Object *var2=...;
    function2();
}
int main()
{
    int var4;
    function1();
    return 0;
}


调用函数时,函数的栈帧被推到栈上,栈向上长出一个栈帧.当函数终止时,其栈帧从程序栈上弹出.栈帧所使用的内存不会被清理,但最终可能被推到程序栈的另一个栈帧覆盖.
动态分配的内存来自堆,堆向下生长,随着内存的分配和释放,堆中会布满碎片.尽管堆是向下生长的,但这只是个大体方向,实际上内存可能会在堆上任意位置分配.

             栈帧的组成

  ①返回地址

               函数完成后要返回的程序内部地址.

②局部数据存储
              为局部变量分配的内存

 ③参数存储
             为函数参数分配的内存

④栈指针和基指针

           栈指针通常指向栈顶部.基指针(帧指针)通常存在并指向栈帧内部的地址,比如返回地址,用来协助访问栈帧内部的元素.这两个指针都不是c指针,它们是运行时系统管理程序栈的地址.如果运行时系统用c实现,这些指针才是c指针.

下面以一个函数来了解栈帧的创建.该函数传递了一个整数数组和数组长度的整数.以及打印参数和局部变量的地址:

float average(int *arr,int size)
{
    int sum;
    printf("arr:%p\n",&arr);
    printf("size:%p\n",&size);
    printf("sum:%p\n",&sum);
    for(int i=0;i<size;i++)
    {
        sum+=arr[i];
    }
    return (sum*1.0f)/size;
}
执行以上代码会输出类似
arr: 0x500
size:0x504
sum: 0x480


参数地址和局部变量地址之间的空挡,保存的是运行时系统管理栈所需要的其他栈帧元素.

系统在创建栈帧时,将参数以跟声明时相反的顺序推到帧上,最后推入局部变量. size在arr之后被推入.通常,接下来会推入函数调用的返回返回地址,然后是局部变量.推入它们的顺序和跟其在代码中列出的顺序相反(和栈特性有关)

 从原理上说,本例子中的栈向上生长,不过栈帧的参数和局部变量以及新栈帧被添加到了低内存地址.栈的实际生长方向和实现有关.

      for 语句中用到的变量i没有包含在栈帧中.C语言把块语句当成微型函数,会在合适的时机将其推入栈和栈中弹出.在本例中,块语句在执行时被推到程序栈中average栈帧上面,执行完后又弹出.
      精确地地址可能会变化,不过顺序一般不变.这一点很重要,因为它可以解释参数和变量内存分配的相对顺序.在调试指针问题时这一点会很有用.如果你不知道栈帧如何分配,这些地址在你看来也毫无意义.
      将栈帧推到程序栈上时,系统可能会耗尽内存,这种情况叫做栈溢出,通常会导致程序非正常终止.要牢记每个线程通常都有自己的程序栈.一个或多个线程访问内存中同一个对象可能会导致冲突. 

没有更多推荐了,返回首页