1.函数递归
1.1什么是递归?
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。
一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,
它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,
递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
这里举一个例子,main函数自己调用自己:
#include <stdio.h>
int main()
{
printf("hehe\n");
main();
return 0;
}
虽然可以运行,无限打印hehe,但是会出现如下错误-》栈溢出
每一次函数调用都会在栈区申请内存空间,无限制申请总会有溢出的时候。
拓展一下,Stack Overflow是程序员用的社区外国网站,相当于知乎,有兴趣可以去看看。
1.2 递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
1.2.1练习1:接受一个整型值(无符号),按照逆顺序打印它的每一位。
逆序非递归方法一:
//输入:1234,输出 4 3 2 1
#include <stdio.h>
int main()
{
unsigned int num = 0;
scanf("%u", &num);//输入无符号整数
while (num)
{
printf("%d ", num % 10);
num = num / 10;
}
//1234%10=4
//1234/10=123
//123%10=3
//123/10=12
//12%10=2
//12/10=1
//1%10=1
//1/10=0
//
//
return 0;
}
随便输入“1234”,如图:
逆序非递归方法二(大同小异):
#include <stdio.h>
void print(unsigned int x)
{
while (x != 0)
{
int d = 0;
d = x % 10;
printf("%d ", d);
x = x / 10;
}
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);//输入无符号整数
print(num);//按照顺序打印num的每一位
return 0;
}
逆序递归:
#include<stdio.h>
void solve(int n)
{
if (n < 10)
{
printf("%d", n);
}
else
{
printf("%d", n % 10);
return solve(n / 10);
}
}
int main(void)
{
int n;
scanf("%d", &n);
solve(n);
return 0;
}
1.2.1练习2:接受一个整型值(无符号),按照顺序打印它的每一位。
不好理解就看这张图,结合F10、F11调试:
方法一:以递归的方式算
//输入:1234,输出 1 2 3 4
#include <stdio.h>
void print(unsigned int n)
{
if (n < 10)
printf("%d ", n);
else
{
print(n/10);//123
printf("%d ", n%10);
}
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);//输入无符号整数
print(num);//按照顺序打印num的每一位
return 0;
}
//方法二:以递归的方式算
//输入:1234,输出 1 2 3 4
#include <stdio.h>
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);//输入无符号整数
print(num);//按照顺序打印num的每一位
return 0;
}
1.3函数栈帧的创建和销毁(引入)
1.3.1函数栈帧
1)寄存器有:
eax
ebx
ecx
edx
ebp
esp
其中ebp、esp这两个寄存器中存放的是地址,这2个地址是用来维护函数栈帧(栈帧-》预开辟空间)的。每一个函数调用,都要在栈区创建一个空间。
为什么寄存器有:eax、ebx、ecx、edx、ebp、esp?这是怎么分类的呢?
可以参考以下链接,有各个寄存器的说明:
https://www.cnblogs.com/chuan0125/p/17054002.html
- 什么是压栈? 给栈顶放一个元素 push
- 什么是出栈? 从栈顶删除一个元素 pop
2)例子:加法函数
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
在运行完代码无误后,点击快捷键Fn+F10来调试,然后按照下图所示操作
在调试时可以从窗口看见main函数被调用,如图:
main函数被谁调用了呢?
在VS2013中,main函数是被"__tmainCRTStartup"函数调用的,而__tmainCRTStartup函数是被“mainCRTStartup ”调用的(我这边用VS2022看不见相关代码页面,在调试完最后一步后VS2013会弹出代码页面,可以发现main函数是被其他函数调用的)。
3)我们来研究一下刚刚的main函数怎么调用的?
在运行完代码无误后,点击快捷键Fn+F10来调试,然后不要动,鼠标右击转到反汇编,按照下图所示操作:
为了方便看地址,把“显示符号名”去掉,如图:
或者直接鼠标右击,取消“显示符号名”:
点击快捷键Fn+F10来调试,第一步压栈,给栈里面放一个元素rbp进去,按图操作:
查看第一个地址是否被压进去:
复制第一个地址
回车
发现第一个地址被压进去了。
再来看mov,把后一个值赋给前一个
sub,减法,这里是rsp减去148h (十六进制显示)
lea ->加载有效地址 load effective address
比如给这里的rbp放了一个地址[rsp+20h]
只是简单介绍一下,这个过程是在为main函数预开辟空间,下面以VS2013的例子来参考(第一个压栈,第二个将esp的值赋给ebp)
为main函数开辟多大空间是由编译器决定的。
这里的word是双字节,dword是doubleword的意思,4个字节。从edi开始,这里39h(ecx次,39次)的空间全都初始化改成eax的内容,也就是0ccccccccc。
开辟完预留栈帧后,接下来到了int a这一步,把0Ah的值赋给[ebp-8]地址,也就是把10这个值赋值给int a
变量需要赋初始值,有时候为什么会打印“烫烫烫烫”是因为放的是“ccccccc"。
局部变量的创建方式就行是先创建栈帧,然后把变量挨个放进去。
变量定义完后,开始调用函数:
1)压栈
传参:
call 调用
为aad函数开辟栈帧,初始化·····着重理解。
函数内部创建的静态变量不在栈区上,所以栈帧创建后不会被销毁。
这里需要注意,寄存器是集成到CPU上的;
硬盘、内存、寄存器都有独立的空间。
1.4 相关练习
1.4.1编写函数不允许创建临时变量,求字符串的长度。
思路:
第一步 --> 求字符串的长度。
#include <stdio.h>
#include<string.h>
int main()
{
char arr[] = "abc";
int len = strlen(arr);
//arr是数组名,首元素地址,是char类型的地址,也是a 的地址,计算/0以前的字符串个数
printf("%d\n", len);
return 0;
}
//打印结果:3
第二步 --> 编写函数,求字符串的长度。
#include <stdio.h>
int my_strlen(char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abc";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
第三步 --> 编写函数,不允许创建临时变量,求字符串的长度。
//#include <stdio.h>
//int my_strlen(char* str)
//{
// while (*str != '\0')
// {
// return 1 + my_strlen(1 + str);
// }
//}
#include <stdio.h>
int my_strlen(char* str)
{
if (*str != '\0')
return 1 + my_strlen(1 + str);
//这里不可以使用str++,因为后置++的意思是先使用后++
//也不可以使用++str,这种写法有副作用,最终结果是++后的,这里不建议,但是运行不会有错
else
return 0;
}
int main()
{
char arr[] = "abc";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
1.5 递归与迭代
1.求n的阶乘。(不考虑溢出)
#include <stdio.h>
//递归的形式
int Fac1(int n)
{
if (n <= 1)
return 1;
else
return n * Fac1(n - 1);
}
//迭代的形式(循环的一种)
int Fac2(int n)
{
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret = ret * i;
}
return ret;
}
int main()
{
int n = 0;
scanf("%d ", &n);
int ret = Fac2(n);
printf("%d\n", ret);
return 0;
}
2.求第n个斐波那契数。(不考虑溢出)
斐波那契数:
1 1 2 3 5 8 13 21 34 55 …
在求第40个斐波那契数的时候,里面把第三个斐波那契数这样的值重复计算了39088169次
所以求斐波那契数是不适合使用递归求解的
#include <stdio.h>
int count = 0;//计数
int fib(int n)
{
if (n == 3)
count++;//计算第三个斐波那契数这样的值被运算了多少次
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("%d\n", ret);
printf("count = %d\n", count);
return 0;
}
//大量重复不利于计算,计算到第50个时很费力
下面介绍第二种高效率方法:
#include <stdio.h>
int fib1(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib1(n);
printf("%d\n", ret);
return 0;
}
//计算到第50个时,计算结果是错误的,因为空间不够,并不是逻辑错误