如果在调用一个函数的过程中又间接或直接地调用该函数本身,称为函数的递归调用。例如,计算阶乘函数f (n)=n!,可以先计算f (n-1)=(n-1)!,而计算f (n-1)时又可以先计算f (n-2)=(n-2)!,这就是递归算法。再入函数是一种可以在函数体内直接或间接调用其自身的一种函数,显然再入函数是可以进行递归调用的。
Keil C51编译器采用一个扩展关键字reentrant,作为定义函数时的选项,需要将一个函数定义为再入函数时,只要在函数名后面加上关键字reentrant即可:
函数类型 函数名(形式参数表)[reentrant]
再入函数可被递归调用,无论何时,包括中断服务函数在内的任何函数都可调用再入函数。与非再入函数的参数传递和局部变量的存储分配方法不同,C51编译器为再入函数生成一个模拟栈,通过这个模拟栈来完成参数传递和存放局部变量。模拟栈所在的存储器空间根据再入函数存储器模式的不同,可以是DATA、PDATA或XDATA存储器空间。当程序中包含多种存储器模式的再入函数时,C51编译器为每种模式单独建立一个模拟栈并独立管理各自的栈指针。对于再入函数有如下规定。
① 再入函数不能传送bit类型的参数,也不能定义一个局部位变量,再入函数不能包括位操作以及8051系列单片机的可位寻址区。
② 与PL/M51兼容的函数不能具有reentrant属性,也不能调用再入函数。
③ 编译时在存储器模式的基础上为再入函数在内部或外部存储器中建立一个模拟堆栈区,称为再入栈。在small模式下再入栈位于IDATA区,在compact模式下再入栈位于PDATA区,在large模式下再入栈位于XDATA区。再入函数的局部变量及参数都被放在再入栈中,从而使再入函数可以进行递归调用。而非再入函数的局部变量被放在再入栈之外的暂存区内,如果对非再入函数进行递归调用,则上次调用时使用的局部变量数据将被覆盖。
④ 在同一个程序中可以定义和使用不同存储器模式的再入函数,任意模式的再入函数不能调用不同模式的再入函数,但可任意调用非再入函数。
⑤ 在参数的传递上,实际参数可以传递给间接调用的再入函数。无再入属性的间接调用函数不能包含调用参数,但是可以使用定义的全局变量来进行参数传递。
例2-28 利用函数的递归调用计算整数的阶乘。
程序执行结果:
在这个程序中定义了一个再入函数fac(n),它是用来计算阶乘n!的函数。在fac()的函数体中又调用了fac()函数本身,因此这是一种函数的递归调用。再入函数在进行递归调用时,新的局部变量和参数在再入栈中重新分配存储单元,并以新的变量重新开始执行。每次递归调用返回时,前面压入的局部变量和参数会从再入栈中弹出,并恢复到上次调用自身的地方继续执行。如果是非再入函数进行递归调用,每次调用函数自身时,上次调用时使用的局部变量数据将被覆盖,因而在递归调用结束时不能得到正确的结果。对于例3.6的程序,如果将函数fac(n)定义成非再入函数,则程序的运行结果为0,显然这是不正确的。
采用函数的递归调用可使程序的结构紧凑,但是递归调用要求采用再入函数,以便利用再入栈来保存有关的局部变量数据,从而要占据较大的内存空间。另外递归调用时对函数的处理速度也比较慢,因此一般情况下应尽量避免采用函数递归调用,定义函数时应尽量避免使用再入属性。