4.3 递归
终于到递归了,前面的函数指针数组的指针你掌握了吗?Let's go on!
递归的定义
递归:
参见“递归”
递归:
如果还是没明白递归是什么意思,参见“递归”
递归就是自己用到自己的意思
递归未必只是自己跟自己,扩大概念
A经理:这事不关我管,找B经理
B经理:这事不关我管,找A经理
两者之间会不断循环往复:这叫做无限递归
也就是说间接的用到自己也叫做递归
简洁而严密,这就是递归定义的优点
阶乘通过递归函数来写成的代码如下:
点击查看代码
#include<stdio.h>
int f(int n)
{
return n == 0 ? 1 : f(n-1)*n;
}
int main()
{
printf("%d\n", f(3));
return 0;
}
由上面的程序启发,那么斐波那契数列地代码表述也可以如下
点击查看代码
#include<stdio.h>
int f(int n)
{
if(n == 1)
return 1;
if(n == 2)
return 1;
return f(n - 1) + f(n - 2);
}
int main()
{
while(true)
{
int a;
scanf("%d", &a);
printf("%d\n", f(a));
}
return 0;
}
也就是说递归地关键是找到前后之间的关系以及起始项,也就是终止条件,比如斐波那契数列中前后项之间的关联就是f(n) = f(n-1) + f(n-2),这就是他们之间的联系,也就是递归的根本条件,自己直接或间接的调用自己
C语言对于递归的支持
为什么C语言真的可以实现自己调用自己呢,这个是跟其中的数据存储结构栈有关
事实上,如果在递归初期查看调用栈,则会发现每次递归调用都会多一个栈帧--和普通的函数调用没有什么不同。的确如此,由于使用了栈帧,C语言自然支持了递归,在C语言的函数中,调用自己和调用其他函数并没有任何本质区别,都是建立新栈帧,传递参数并修改当前代码行。在函数体执行完毕后删除栈帧,处理返回值并修改当前代码行
由于使用了调用栈,C语言支持递归,在C语言中,调用自己和调用其他函数并没有本质不同,因为他的每一次栈帧都是不同的
也就是说:递归调用时新建了一个栈帧,并且跳转到了函数开头处执行,尽管同一时刻可以有多个栈帧,但“当前代码行”只有一个,也就是说计算机实际上任何时刻都只能处理一件事,类似于多线程的实现,也是在多个线程之间公用资源来实现的
设计递归的重点在于如何给下级安排工作
段错误与栈溢出
到了现在,对于C语言的介绍接近了尾声,但是我们还没有真正测试f函数,或许会想当然的认为当n足够大的时候不就是乘法溢出吗,但是如果输入f(100000000)会发现根本没有输出,程序会异常退出,并不会输出一个显然不正确的值
如果使用gdb调试的时候会发现gdb显示程序接收到了SIGSEGV信号--段错误
关于段错误的解释如下:
编译后产生的可执行文件里都保存着些什么内容呢?这是和操作系统有关的。
例如:UNIX/Linux用的ELF格式,DOS下用的是COFF格式,而Windows用的是PE文件格式(由COFF扩充而来),这些格式不尽相同,但都有一个共同的概念--段。
‘段’(segmentation)是指二进制文件内的区域,所有某种特定类型信息被保存在里面
可以用size程序(Linux和Windows下的MinGW中都有这个程序)得到可执行文件中各个段的大小
size 程序名.exe
可以在dos系统下输入这段语句来查看可执行文件中的段的分配
在可执行文件中,正文段(Text Segment)用于储存指令,数据段(Data Segment)用于储存已初始化的全局变量,BSS段(BSS Segment)用于储存未赋值的全局变量所需要的空间
size出来的显示数据标题为
text data bss dec hex filename
text 正文段
data 数据段
bss bss段
dec 总大小
hex 总大小的十六进制表示
filename 文件名称
注意:调用栈并不储存在可执行文件中,而是在运行时创建,调用栈所在的段成为堆栈段(Stack Segment)。和其他段一样,堆栈段也有自己的大小,不能被越界访问,否则就会出现段错误(Segmentation Fault)
这样,前面的错误就不难理解了,每次递归调用都需要往调用栈里增加一个栈帧,久而久之就越界了,这种情况叫做栈溢出(Stack Overflow)
在运行时,程序会动态创建一个堆栈段,里面存放着调用栈,因此保存着函数的调用关系和局部变量(栈帧是记录函数信息的,而调用关系和局部变量应该就是因为在函数里面,所以也就是在堆栈段中)
栈空间的大小和操作系统相关。在Linux中,栈大小是由系统命令ulimit指定的,eg:ulimit -a显示当前站的大小,而ulimit -s 32768将把栈大小指定为32MB.但在Windows中,栈大小是储存在可执行文件中的,使用gcc可以这样指定可执行文件的栈大小,
gcc -Wl,--stack=16777216
,这样栈大小就变为16MB
在linux中,栈大小并没有储存在可执行程序中,只能用ulimit命令修改,在Windows中,栈大小储存在可执行程序中,用gcc编译时可以通过gcc -Wl,--stack=
这就是为什么建议把较大的数组放在main函数外了,注意,局部变量也是放在堆栈段的。栈溢出有时不一定是递归调用太多,也可能是局部变量太大了,只要总大小超过了允许的范围,就会产生栈溢出
总的来说栈帧中所包含的数据都会占据堆栈段的空间,一旦里面的数据越界访问数据,就会产生段错误,使程序异常退出
注意:实际上,栈大小是由连接程序ld指定的,gcc编译参数-Wl的作用正式把气候的参数(-stack==
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)