前言:上期我们介绍了函数的概念,库函数,自定义函数等等,这期我们来介绍一下函数的嵌套调用,链式访问,和函数递归。
传送门:上一篇文章在这里函数上
一,函数的嵌套调用
听到函数嵌套不知你是否会想起,条件嵌套,和循环嵌套;条件嵌套:是多个条件语句比如说多个if语句嵌套在一起;循环嵌套:是多个循环语句比如for循环while循环嵌套在一起;那函数的嵌套会是多个函数嵌套在一起吗?究竟是怎么嵌套的呢?这就是接下来要介绍的函数的嵌套调用:
先来看一段代码:
#include<stdio.h>
//定义一个一次打印hehe的函数
void one_printf()
{
printf("hehe\n");
}
//定义一个连续3次打印hehe
void three_printf()
{
for(int i=0;i<3;i++)
{
one_printf();//调用一次打印hehe的函数
}
}
int main()
{
three_printf();//调用三次打印hehe的函数
return 0;
}
我们先来分析一下这段代码:首先这段代码有3个函数一个主函数
(main()函数)
和两个自定义函数分别是one_printf()
一次打印函数和three_printf()
三次打印函数。细心点会发现有两处调用第一处在main函数里调用了three_printf()还有在three_printf()里调用了one_printf()。
从运行结果上看,打印三次hehe的逻辑就是程序1,先进入到主函数调用 three_printf()
这个三次打印函数 2,程序入到这个函数后紧接着三次调用 one_printf()
这个一次打印函数(注意有个for循环循环3次),而每一次调用这一次打印函数就会打印一次hehe 3,打印三次所以就得到三个hehe。你可以将他想象成一个个的链接,链接里面还有链接,一个链接里面可以放一个或多个链接。这便是函数的嵌套调用。
注意:函数之间可以嵌套调用但不能嵌套定义!
二,链式访问
第一次看到这个名字大家是否会感到疑惑,什么是链式访问呢?先给大家来道题请看代码:
#include<stdio.h>
int main()
{
printf("%d",printf("%d",printf("%d",43)));
return 0;
}
大家觉得这段代码执行的结果是什么?我这里就直接公布答案了
没错,答案是4321!是不是很神奇,如果你答对了恭喜你,如果你答错了就跟我一起来分析一下为什么是这个结果,首先正常情况下我们使用
printf("%d",)
逗号后面应该要是一个值;再观察上面共有3个printf
从右往左数,第一个printf打印43无疑了;那么我们大胆猜测第二个就打印2,第三个就打印3,那事实是不是这样呢?我们来了解了解printf
函数的返回值就知道了:
从图中我们能看到return value 就是返回值的意思,就是说printf的返回值是按照打印数字的个数来返回,打印几个数字就返回几;第一个printf
打印了43这两个数字;那么第二个printf
打印的就是第一个printf
的返回值2;第三个printf
则打印第二个的返回值1,这与我们之前的猜想是完全吻合的,就像之前所说的链式访问就像链条一样连接在一起。
- 总结:所谓链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数的链式访问。 注意,能链式访问的前提是,函数必须要有返回值,没有返回值的函数不能链式访问。
了解完函数的嵌套调用和链式访问后,大家会不会有个疑问既然函数内部能够调用其他的函数,那么函数内部能不能调用函数本身呢?答案是肯定的,这也是接下来本篇文章的重头戏,我将重点介绍 函数递归和迭代,函数如何调用它本身以及怎么样替换递归。我们马上开始:
三,函数递归
首先我们来一起剖析一下函数递归这个名词的意思是什么?
1. 函数:笼统的说就是我们的自定义函数。
2. 递:层层递进,你也可以理解为数学上的递推函数。
3. 归:很明显就是回归。
看到这里你是否又会有些疑惑怎么函数怎么能递进呢?函数又怎么能回归呢?别着急要回答上面两个问题我们先来回答到底什么是递归这个问题?
举例1
#include<stdio.h>
int main()
{
printf("hehe\n");
main();
return 0;
}
这里运行的结果其实是死循环我只截取了部分出来,那为什么会循环呢?我们不妨画一个图来解释:
- 总结:从上面的例子和对例子的分析不难看出递归就是函数自己调用自己。
- 注意:上述就是⼀个简单的递归程序,只不过上⾯的递归只是为了演示递归的基本形式,不是为了解决问题,代码最终也会陷入死递归。
举例2
了解完什么是递归我们就再来举一个例子,求n的阶乘:
我们画个图先分析一下思路:
求n的阶乘的逻辑是,我们要先从上图的左侧通过找规律关键要得出 n!=n-1!*n
这个递推式这是其一;接下来就是要得到递归停下来的条件当n等于0时规定0!等于1这点很重要这就是递归停下来的条件这是其二;有了 n!=n-1!*n
这个递推式子就可以写出上图右侧的函数递推形式。注意:下面的代码一定要结合上图来看!
#include<stdio.h>
//用递归求n!
int Fac(int n)
{
if (n == 0)//递归停下来的条件
{
return 1;
}
else
{
return n * Fac(n - 1);//递推式子
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fac(n);//定义ret储存Fac函数的返回值
printf("%d\n", ret);
return 0;
}
此时我们输入3就会发现得到6,3的阶乘等于6所以是正确的,通过上面的分析我们就很容易写出递归的代码。初学者刚开始学习的时候总会觉得很难理解,其实不用把他看得很复杂你就想什么已知,什么未知就行了,已知的不用管;未知的就让函数调用自己去求就好了。先记住这句话函数递归其实就是一个从未知去寻找已知的过程。我们画图举例:
图画得有些不好还请见谅,上图应该能够很好的回答了上面如何层层递进和回归的问题了;另外如果你理解了让函数自己调用自己解决未知这种理解方式你会觉得递归其实没有你想象中得那么复杂。下面再给出一张更加形象的图供各位读者们理解:
总结:通过上面的实例应该能能慢慢体会到递归的思想:
1. 递归就是把一个大型问题层层转化为一个与原问题相似,但规模较小的子问题求解(比如上面层层求未知的例子);直到问题不能再被拆分递归就结束了。所以递归的思考方式就是把大事化小,逐个击破的过程。
递归的限制条件:从我们最开始最简单递归的例子到打印n的阶乘的例子,第一个例子最终结束和第二个例子结束的条件是不一样的第一个例子递归停下来的条件是栈溢出(这里不需要深究),第二个例子是递归最终达到n为0这个条件从而递归结束。
2. 由此我们得出递归是有条件的,但满足这个条件的时候递归便不再继续,(递归在一些条件下会停下来);每次递归调用后都会越来越逼近那个要跳出的条件。
如果你还是觉得不是很理解半懂半懵,接下来我就再来举一个例子让你慢慢体会这种让函数调用自己来寻找未知的理解方式。
举例3
求n的k次方,下面的例子以2的k次方来举例,即n等于2:
以上就是实现n的k次方的基本思路;下面给出代码,我们来看看正真落实到代码上该怎样写:
#include<stdio.h>
#include<math.h>
//用函数递归实现n的k次方
int An(int k)
{
if (k == 1)//上图的条件1
{
return 1;
}
else//k>1这个条件
{
return An(k - 1)+1;
}
}
int main()
{
int n = 0;
int k = 0;
scanf("%d %d", &n,&k);//输入底数 和指数
int ret = 0;//定义ret来储存计算结果 注意An(k)只是返回指数 即几次方
ret = pow(n,An(k));//使用了pow这个求次方的库函数要引用头文件
printf("%d ", ret);
return 0;
}
我们以2的3次方举例画图来分析一下代码是怎样运行的:
其实求n的k次方,直接调用pow(n,k)就行了,这里只是为了展示递归的思想,方便大家理解这种逐层求未知的思路。说到这,递归这块内容就算是已经结束了,递归这种思维是需要经过训练的。前期想不到思路也不必怀疑自己慢慢去领悟,等到后面积累了大量题目水到渠成,自然就会了。
四,递归与迭代
递归是一种很好的编程技巧,仅用少量的代码就能完成复杂的计算任务,但是和很多技巧一样递归很容易被误用,像第一个例子一样死循环引发栈溢出最终导致程序崩溃这是我们不想看到的。
- 在C语⾔中每⼀次函数调⽤,都需要为本次函数调⽤在内存的栈区,申请⼀块内存空间来保存函数调 ⽤期间的各种局部变量的值,这块空间被称为运⾏时堆栈,或者函数栈帧。 (关于函数栈帧的内容将在下一次再着重介绍)
- 函数不返回,函数对应的栈帧空间就⼀直占用,所以如果函数调⽤中存在递归调⽤的话,每⼀次递归 函数调⽤都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。 所以如果采⽤函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出的问题。
可见递归并不是万能的所以我们有了迭代,就是用循环来替代递归,还拿上面的第二个例子求n的阶乘来举例这次我们使用循环来替代:
//用循环求n!
int main()
{
int n = 0;
scanf("%d", &n);
int ret = 1;
for (int i = 1;i <= n;i++)
{
ret = ret * i;
}
printf("%d", ret);
return 0;
}
输入5,5的阶乘为120说明运算是正确的,代码的逻辑是用循环来产生后面的数,比如我们求5的阶乘,i从1到5,ret初始值为1,从1乘到5这可不就是5的阶乘嘛。(注意ret初始值一定是1不然程序就出bug了)。由此看来迭代好像也不弱于递归啊,哈哈哈。
那这时又有人要问了什么时候使用迭代呢?举个例子求第n个斐波那契数的时候:
这里我们直接给出代码,感兴趣的可以自行研究:
#include<stdio.h>
//使用递归求n个斐波那契数
int Fib(int n)
{
if (n <= 2)
{
return 1;
}
else
{
return Fib(n - 1) + Fib(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = 0;
ret = Fib(n);
printf("%d", ret);
return 0;
}
我们再给出迭代法:
#include<stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int a = 1;
int b = 1;
int c = 0;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
printf("%d", c);
return 0;
}
由于第n个斐波那契数使用递归但n一旦大于50递归就会消耗过多的内存算不出来,而迭代法则很快能够计算出答案虽然结果不一定准确但是效率非常高,大家可以复制这两段代码自己试一试就知道了。
五,总结
看到这里关于递归与迭代基本上就进入尾声了,下面我们来总结一下:
1. 递归
- 简洁,往往用少量的代码就完成了大量复杂的计算。
- 但这要以牺牲较多的内存作为代价,有运行时的开销(内存和时间)有时候会以影响效率,也可能会导致栈溢出。
2. 迭代
- 效率高。
- 但有时代码写起来比较复杂。
好了以上就是本本文的所有内容啦,内容很多能坚持看到这的读者很不容易给你们点赞!文章还有瑕疵请各位大佬们谅解如有错误欢迎指出,同时如果本文能帮到您那会是我最大的荣幸!制作不易还请各位大佬三连支持一下,感谢各位大佬的支持!!!
传送门:上一篇文章在这里函数上