要深入理解函数及其和指针的结合应用,需要理解程序栈.大部分的现代的块结构语言,比如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栈帧上面,执行完后又弹出.
精确地地址可能会变化,不过顺序一般不变.这一点很重要,因为它可以解释参数和变量内存分配的相对顺序.在调试指针问题时这一点会很有用.如果你不知道栈帧如何分配,这些地址在你看来也毫无意义.
将栈帧推到程序栈上时,系统可能会耗尽内存,这种情况叫做栈溢出,通常会导致程序非正常终止.要牢记每个线程通常都有自己的程序栈.一个或多个线程访问内存中同一个对象可能会导致冲突.