C语言笔记7:函数递归与函数栈帧

C语言笔记7:函数递归与函数栈帧

一、什么是递归

递归即为递推和回归,是一种解决问题的办法。在C语言中体现为函数自己调用自己。

递归思想

将一个大型复杂问题转化为一个与原问题类似,但是规模较小的子问题来求解。所以递归就是大事化小。

递归的限制条件

递归函数内必须存在一个限制条件以防止递归无限进行下去。

二、斐波那契数说明递归的局限

斐波那契数的公式:

c 复制代码
int Fib(int n)
{
	if(n <= 2)
		return 1;
	else
		return Fib(n - 1) + Fib(n - 2);
}

根据上面公式很容易写出斐波那契数的递归解

但是我们带入一个数,假设n = 5:递归展开示意图


斐波那契数使用递归求解,其递归展开次数是O(2^n)的
事实上在实际中,能使用迭代尽量应该使用迭代,递归的好处只是有些时候一些复杂的逻辑,使用递归的方法能让代码变得简洁。

三、拓展递归问题

青蛙跳台阶

一只青蛙要跳n级台阶,青蛙每次可以跳1级或2级台阶,请问青蛙跳上n级台阶有几种跳法?

利用递归的思想,大规模问题转化成相似问题的小规模问题,跳上n级台阶有几种跳法?

等于跳上n - 2级台阶的跳法总数 + 跳上n - 1级台阶的跳法总数。

特殊情况:当台阶数为0或者为1的时候,跳法数是1。

代码如下:

c 复制代码
int frog(int n)
{	
	if(n <= 1)
		return 1;
	
	return frog(n - 1) + frog(n - 2);
}

汉诺塔问题

有三根柱子,标记为 ABC

在柱子 A 上,有 n 个大小不同的圆盘,从下到上按照从大到小的顺序叠放(最大的在底部,最小的在顶部)。

目标

将所有圆盘从柱子 A 移动到柱子 C

移动规则:

  1. 每次只能移动一个圆盘(不能一次移动多个)
  2. 只能移动最顶端的圆盘(不能从中间抽取)
  3. 任何时刻,较大的圆盘不能放在较小的圆盘上面(即每根柱子上的圆盘必须保持从大到小、从下到上的顺序)


用一次移动来理解这个问题:

想要移动圆盘到C,那么想象一下C柱的情况,肯定有一步是把最大的盘从A移动到C,那么剩下两个盘肯定就在B柱,所以应该过程如下:


问:把三个盘中最大的盘挪到C柱用了几步?

答:用的步数等于把两个盘移动到另一个柱次数 + 1

问:总移动次数等于几次?

答:等于把最大的盘移动到C柱 + 移动剩下两个盘的次数

代码:

c 复制代码
int Hanoi(int n)
{
	if(n <= 1)
		return 1;
		
	return Hanoi(n - 1)*2 + 1;
}

四、函数栈帧的创建和销毁

环境:

Visual Studio 2022 x86 项目->属性->

main函数反汇编:

c 复制代码
int main()
{
004017E0  push        ebp  
004017E1  mov         ebp,esp  
004017E3  sub         esp,0E4h  
004017E9  push        ebx  
004017EA  push        esi  
004017EB  push        edi  
004017EC  lea         edi,[ebp-24h]  
004017EF  mov         ecx,9  
004017F4  mov         eax,0CCCCCCCCh  
004017F9  rep stos    dword ptr es:[edi]   
	int a = 10;
00401806  mov         dword ptr [a],0Ah  
	int b = 20;
0040180D  mov         dword ptr [b],14h  
	int ret = 0;
00401814  mov         dword ptr [ret],0  

	ret = Add(a, b);
0040181B  mov         eax,dword ptr [b]  
0040181E  push        eax  
0040181F  mov         ecx,dword ptr [a]  
00401822  push        ecx  
00401823  call        _Add (04013B6h)  
00401828  add         esp,8  
0040182B  mov         dword ptr [ret],eax  

	return 0;
0040182E  xor         eax,eax  
}
00401830  pop         edi  
00401831  pop         esi  
00401832  pop         ebx  
00401833  add         esp,0E4h  
00401839  cmp         ebp,esp  
0040183B  call        __RTC_CheckEsp (0401244h)  
00401840  mov         esp,ebp  
00401842  pop         ebp  
00401843  ret  

main函数也是被调用的:

main函数创建栈帧

c 复制代码
004017E0  push        ebp  
004017E1  mov         ebp,esp  
004017E3  sub         esp,0E4h  
004017E9  push        ebx  
004017EA  push        esi  
004017EB  push        edi  
004017EC  lea         edi,[ebp-24h]  
004017EF  mov         ecx,9  
004017F4  mov         eax,0CCCCCCCCh  
004017F9  rep stos    dword ptr es:[edi]   

先看这两段

c 复制代码
004017E0  push        ebp  
004017E1  mov         ebp,esp  


esp原本指向0x00AFF7A4,压入ebp的值后,esp的值减了4,向低地址处移动。然后把ebp栈底指针的值修改为esp的值。
再看这四段

c 复制代码
004017E3  sub         esp,0E4h  
004017E9  push        ebx  
004017EA  push        esi  
004017EB  push        edi  

esp先减少0E4h开辟出一段空间,然后压入三个值
再看这四段

复制代码
004017EC  lea         edi,[ebp-24h]  
004017EF  mov         ecx,9  
004017F4  mov         eax,0CCCCCCCCh  
004017F9  rep stos    dword ptr es:[edi]  

将[ebp-24h,ebp)的值初始化为cc

局部变量的创建和初始化

c 复制代码
	int a = 10;
00401806  mov         dword ptr [a],0Ah  
	int b = 20;
0040180D  mov         dword ptr [b],14h  
	int ret = 0;
00401814  mov         dword ptr [ret],0  

传参

c 复制代码
	ret = Add(a, b);
0040181B  mov         eax,dword ptr [b]  
0040181E  push        eax  
0040181F  mov         ecx,dword ptr [a]  
00401822  push        ecx  

调用函数,维护新栈帧,记录下一条指令的地址

复制代码
00401823  call        _Add (04013B6h)  
00401828  add         esp,8  

Add函数:

复制代码
int Add(int x, int y)
{
00401770  push        ebp  
00401771  mov         ebp,esp  
00401773  sub         esp,0CCh  
00401779  push        ebx  
0040177A  push        esi  
0040177B  push        edi  
0040177C  lea         edi,[ebp-0Ch]  
0040177F  mov         ecx,3  
00401784  mov         eax,0CCCCCCCCh  
00401789  rep stos    dword ptr es:[edi]  
	int z = 0; 
00401796  mov         dword ptr [ebp-8],0  
	z = x + y;
0040179D  mov         eax,dword ptr [ebp+8]  
004017A0  add         eax,dword ptr [ebp+0Ch]  
004017A3  mov         dword ptr [ebp-8],eax  
	return z;
004017A6  mov         eax,dword ptr [ebp-8]  
}
004017A9  pop         edi  
004017AA  pop         esi  
004017AB  pop         ebx  
004017AC  add         esp,0CCh  
004017B2  cmp         ebp,esp  
004017B4  call        00401244  
004017B9  mov         esp,ebp  
004017BB  pop         ebp  
004017BC  ret  

Add函数初始化栈帧:

c 复制代码
int Add(int x, int y)
{
00401770  push        ebp  
00401771  mov         ebp,esp  
00401773  sub         esp,0CCh  
00401779  push        ebx  
0040177A  push        esi  
0040177B  push        edi  
0040177C  lea         edi,[ebp-0Ch]  
0040177F  mov         ecx,3  
00401784  mov         eax,0CCCCCCCCh  
00401789  rep stos    dword ptr es:[edi]  

Add函数使用参数计算,返回值写入寄存器eax

c 复制代码
	int z = 0; 
00401796  mov         dword ptr [ebp-8],0  
	z = x + y;
0040179D  mov         eax,dword ptr [ebp+8]  
004017A0  add         eax,dword ptr [ebp+0Ch]  
004017A3  mov         dword ptr [ebp-8],eax  
	return z;
004017A6  mov         eax,dword ptr [ebp-8]  

Add函数栈帧销毁:

c 复制代码
004017A9  pop         edi  
004017AA  pop         esi  
004017AB  pop         ebx  
004017AC  add         esp,0CCh  
004017B2  cmp         ebp,esp  
004017B4  call        00401244  
004017B9  mov         esp,ebp  
004017BB  pop         ebp  
004017BC  ret  

esp一路增加,栈空间不断减少直到和ebp一致:

pop ebp :首先esp指向下一个位置,然后ebp指向main函数的栈底指针

ret:指令回到调用位置处的下一条指令地址,esp再次指向下一个位置

调用函数处收尾:

c 复制代码
	ret = Add(a, b);
0040181B  mov         eax,dword ptr [ebp-14h]  
0040181E  push        eax  
0040181F  mov         ecx,dword ptr [ebp-8]  
00401822  push        ecx  
00401823  call        004013B6  
00401828  add         esp,8  
0040182B  mov         dword ptr [ebp-20h],eax  

完整图:

相关推荐
你怎么知道我是队长11 小时前
C语言---typedef
c语言·c++·算法
phltxy12 小时前
从零入门JavaScript:基础语法全解析
开发语言·javascript
带土112 小时前
5. enum(枚举)关键字在C/C++中的作用
c语言·c++
天“码”行空12 小时前
java面向对象的三大特性之一多态
java·开发语言·jvm
odoo中国13 小时前
Odoo 19 模块结构概述
开发语言·python·module·odoo·核心组件·py文件按
代码N年归来仍是新手村成员14 小时前
【Java转Go】即时通信系统代码分析(一)基础Server 构建
java·开发语言·golang
Z1Jxxx14 小时前
01序列01序列
开发语言·c++·算法
沐知全栈开发14 小时前
C语言中的强制类型转换
开发语言
丝斯201115 小时前
AI学习笔记整理(42)——NLP之大规模预训练模型Transformer
人工智能·笔记·学习
关于不上作者榜就原神启动那件事15 小时前
Java中大量数据Excel导入导出的实现方案
java·开发语言·excel