在学习函数嵌套调用和递归调用的时候如果不知道每个函数调用干了什么就会很难理解函数连续调用自己或者其他函数到底怎么连续传参的。函数调用其实是两个过程:传入实参给形参并执行被调用函数,调用完成后回到上次被调用函数的下一行位置继续执行原函数,理解这个过程对理解递归调用很重要。
函数堆栈调用内部原理可观看,这里指列出结论部分【C++】二、函数堆栈调用、返回值、调用约定_聪聪菜的睡不着的博客-CSDN博客
上图是C函数,下图是内存中函数入栈和出栈情况
sum函数调用时候入栈完成
sum函数调用完出栈图
了解了整个函数堆栈调用过程,我们可以对这四个问题进行一个解答:
【问题一:】被调用方执行完后,怎么回退到调用方函数?
答:在调用方函数开辟栈帧时,第一步就是将调用方函数的栈底地址压入栈中,这样回退时只需要将其弹出给ebp即可,这样就可以回到调用方函数。
【问题二:】被调用方函数执行完回到调用方函数后,怎么知道延下一行继续执行?
答:调用方函数在开辟被调用方函数栈帧前,call指令将调用方的下一行指令压入函数栈帧,这样在清栈时,弹出下一行指令即可接着下一行继续执行。
【问题三:】返回值是由什么带出来的?
在该函数中,大小为4字节的返回值由寄存器带出。
【问题四:】形参需不需要开辟内存?如果形参开辟内存,在哪里开辟的?
答:需要,调用方函数会开在自己的栈帧上开辟内存。
那我们把一个函数的开栈和清理栈的步骤进行总结:
【函数开栈过程:】
压入实参, 调用函数方开辟形参空间并赋值,可以看到是从右往左顺序依次入栈的。
压入下一行指令地址,方便被调用方函数处理完能沿着调用方下一行指令继续执行。
压入调用方栈底地址,方便被调用方函数处理完成能够回退到调用方。
预留被调用方函数的活动空间,并做cccc cccc的初始化。
【函数清栈过程:】
清理被调用函数预留的活动空间
出栈ebp,栈底指针,ebp回退到被调用方栈帧上。
出栈下一行指令寄存器,函数调用完成,能沿着下一行指令继续执行。
清理形参。
返回值
函数返回值不能返回局部变量的地址。函数调用完成,栈帧结束,在函数清栈的过程中,保存临时变量的函数空间就被回收了,系统可以重新分配这块空间,那么就有可能被其他函数重新覆盖。虽然你得到了这块空间的地址,但是这是错误的:
其一这是一种越界操作,你访问了别人的内存空间,但是系统无法检测。
其二此时地址中保存的数值并不是原来函数的,可能已经被新的函数进行初始化了。
了解了函数开栈和清栈过程,再去看下嵌套调用和递归调用流程
函数的嵌套调用流程
调用一个函数的过程中,又可以调用另一个函数。
上图表示了两层嵌套的情形。其执行过程是:
- 1.执行 main函数 中调用 a函数 的语句时,即转去执行a函数;
- 2.在 a函数 中调用 b函数 时,又转去执行 b函数;
- 3.在 b函数 执行完毕返回 a函数 的断点继续执行;
- 4.在 a函数 执行完毕返回 main函数 的断点继续执行。
单纯看程序代码,只能1、2、3、4、5的过程,实际上要理解6,7,8函数怎么出栈的,出栈传回了什么返回值给调用函数的下一行。
函数的递归调用流程
在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。
【例】有5个人坐在一起,问第5个人多少岁?他说比第4个人大两岁。问第4个人岁数,他说比第3个人大两岁。问第3个人,又说比第2个人大两岁。问第2个人,说比第1个人大两岁。最后问第1个人,他说是10岁。请问第5个人多大?
每一个人的年龄都比其前1个人的年龄大两岁。即:
age(5)=age(4)+2
age(4)=age(3)+2
age(3)=age(2)+2
age(2)=age(1)+2
age(1)=10
可以用式子表述如下:
age(n)=10 (n=1)
age(n)=age(n-1)+2 (n>1)
代码如下
#include <stdio.h>
main() {
int add(int a);printf("a36 = %d\n", add(36));
}int add(int a) {
int sum;
if (a == 1) {
sum = 10;
}
else {
sum = add(a-1) + 2;
}
return sum;
}
可以看到,当n>1时,求第n个人的年龄的公式是相同的。因此可以用一个函数表示上述关系。图4.11表示求第5个人年龄的过程。
函数递归更加要明白函数调用入栈和函数清栈的流程,左半部分可以看成连续函数调用,在 sum = add(a-1) + 2语句之前必须有结束递归的判断条件。右半部分就是连续函数清栈的过程,要特别注意 sum = add(a-1) + 2语句及return语句返回之间部分,每一个函数清栈这部分都会执行一遍,可以理解成循环操作这个过程,循环次数就是递归的次数。