小知识,大挑战!本文正在参与「程序员必备小知识」创作活动
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
C语言允许函数自身调用自身,这种调用就被称为递归。好多人刚开始学习递归的时候,往往被一层层嵌套调用搞糊涂了,搞不清楚到底是怎么调用的?现在就通过一个小例子来演示一下,递归调用时,函数是如何运行的。
void up_and_down(int n)
{
printf("---- Level %d: n location 0x%p \r\n",n,&n);
if(n<4)
up_and_down(n + 1);
printf("**** Level %d: n location 0x%p \r\n",n,&n);
}
int main()
{
up_and_down(1);
system("pause");
return 0;
}
首先调用函数给n赋值为1,然后当n小于4时,给n加1,然后继续调用这个函数。直到n的值大于4,就退出函数,运行结果如下:
从打印结果可以看出n的值为1、2、3、4依次递增,然后又从4、3、2、1依次递减,最后退出程序,那么这个程序执行的流程是什么呢?下面用一张图来分析一下。
下面一步一步来分析:
首先n=1,执行函数体内的第一条打印语句。然后执行if语句,由于n当前值为1,小于4,所以又调用函数本身,将1+1的值传递给函数。
接下来执行函数体内的第一条打印语句。然后执行if语句,由于n当前值为2,小于4,所以又调用函数本身,将2+1的值传递给函数。
继续执行函数体内的第一条打印语句。然后执行if语句,由于n当前值为3,小于4,所以又调用函数本身,将3+1的值传递给函数。
继续执行函数体内的第一条打印语句。然后执行if语句,由于n当前值为4,不小于4,所以if条件不成立,不会再调用up_and_down()函数了,而是执行函数体内最后一条打印语句。当这条打印语句执行完之后,退出函数。 由于第4次条用函数,是从第三次调用函数中来的,所以退出第四次函数后,程序就会回到第三次函数调用它的地方。
第四次函数退出后,相当于就返回到了第三次函数的up_and_down(3 + 1);这个位置,由于这个函数已经执行完了,所以代码继续执行,打印最后一行的打印语句。
同样当第三次调用的函数执行完成之后,程序就会返回到调用的位置,也就是返回到第二次调用的函数中。
程序返回到up_and_down(2 + 1);这个语句之后,然后继续执行最后一行的打印语句。当最后一行的打印语句执行完成之后,第二次调用的函数就会返回到调用它的地方,也就是返回到第一次调用函数的内部。
程序返回到up_and_down(1 + 1);这个语句之后,然后继续执行最后一行的打印语句。当最后一行的打印语句执行完成之后,程序返回到第一次调用这个函数的地方。而第一次调用这个函数是在main()函数中,所以程序就会退回到main()函数中。由于main()函数中没有执行其他语句。所以程序整个流程就运行结束了。
通过打印的结果也可以看出,首先打印的都是函数体内第一个行的打印语句,当4次函数调用都完成后,才继续打印函数体中的最后一行打印语句。通过n的地址也可以看出来,每次新调用函数的时候,都会给n重新申请一个地址,然后当函数退出的时候,再将申请的地址释放掉。
由此可见递归函数在执行的时候,还是比较浪费内存空间的,对于资源很有限的单片机来说,还是尽量少用递归函数。难道递归函数真的一点优势都没有吗?当然是有的,通过对于上面的例子观察可以发现,递归函数执行时是先进后出的原则,那么对于需要倒叙执行的程序来说,使用递归函数是非常方便的。 比如在进行进制转换的时候,第一次计算的值要放到结果的最后一位,最后一次计算的值要放在结果的第一位。
比如现在要将10转换为二进制数。计算过程如下:
10%2 ---- 0
10/2=5
5%2 ----- 1
5/2=2
2%2 ----- 0
2/2=1
1%2 ----- 1
首先将10除以2取余数,余数为0,那么这个0就是二转进制的最低位。接下来将10除以2去整,然后再用结果除以2取余,这个就是二进制的倒数第二位,安装这样的规律,依次计算,直到除数结果为1.
也就是传递的参数变为1时,结束递归的调用。 这样10转换为二进制就时 1010,这个数字刚好是计算的数字倒叙排列。
下面就通过递归来实现这个功能
void to_binary(unsigned long n)
{
int r;
r = n % 2;
if(n >= 2)
to_binary(n/2);
putchar(r == 0?'0':'1');
return;
}
int main()
{
to_binary(100);
printf("\r\n");
system("pause");
return 0;
}
定义一个函数来打印二进制的值,首先用这个数字除以2取余,然后将数字除以2取整之后继续调用此函数,运行结果如下:
将100转换为二进制值
程序打印的结果和实际计算的结果一样,说明程序功能是正常的。为了方便查看,可以将函数执行过程打印出来。
为了方便查看函数执行效率,这里可以将函数执行的时间打印出来。 测试代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
void to_binary(unsigned long n)
{
int r;
r = n % 2;
printf("n-%d,r-%d\r\n",n,r);
if(n >= 2)
to_binary(n/2);
putchar(r == 0?'0':'1');
return;
}
typedef union _LARGE_INTEGER
{
struct
{
long LowPart ;// 4字节整型数
long HighPart;// 4字节整型数
};
long long QuadPart;// 8字节整型数
} LARGE_INTEGER;
int main(int argc, char *argv[])
{
int i = 0,val = 0;
clock_t startTime,endTime;
int time = 0;
LARGE_INTEGER secondcount= {0};
LARGE_INTEGER startcount= {0};
LARGE_INTEGER stopcount= {0};
QueryPerformanceFrequency(&secondcount); //获取每秒多少CPU Performance Tick 单位us
//printf(" 3:系统计数频率为: %d \r\n",secondcount.QuadPart);
QueryPerformanceCounter(&startcount); //计时开始
to_binary(100); //递归调用
QueryPerformanceCounter(&stopcount); //计时结束
time=( ((stopcount.QuadPart - startcount.QuadPart)*1000*1000)/secondcount.QuadPart);
printf(" \r\n\r\n 程序运行时间为: %d us\r\n\r\n",time);
system("pause");
return 0;
}
这里添加了一个测试递归调用函数执行时间,测试的原理就是,在函数执行前记录一下系统的计数值,然后执行完成之后,在记录一下系统计数值,这两个值的差就是系统的执行时间。 执行结果如下:
可以看到计算一次100的二进制数需要1.163ms,这对于计算机来说太慢了。如果计算一个更大的数,那岂不是更费时间,用数字100000000测试一下。
花费的时间为5.1ms左右,可见数字越大函数嵌套的就越深,就会越费内存。执行时间也就越久。
以后再使用递归函数的时候,一定要注意使用环境,否则如果在一个资源比较少的单片机上,调用了一个嵌套比较深的递归函数,很可能计算到一半系统资源就会被耗尽,导致程序崩溃,而且很难查找到原因。