递归是一种在移动到下一次迭代之前隐式保留本地状态的迭代。通过考虑一个接一个地相互调用的常规函数,很容易理解这一点:
void iteration_2 (int x) {
/* ... */
}
void iteration_1 (int x) {
if (x > 0) return;
iteration_2(x + 1);
}
void iteration_0 (int x) {
if (x > 0) return;
iteration_1(x + 1);
}
每个iteration_#()
基本上都是相同的,但是每个都有它自己的x
,并且每个都记住哪个函数调用了它,所以它可以在它调用的函数完成时正确地返回给调用者。当程序转换为递归版本时,这个概念不会改变:
void iteration (int x) {
if (x > 0) return;
iteration(x + 1);
}
如果停止条件(对函数的if
检查return
)被移除,则迭代变为无限。递归没有返回。因此,为每个连续的函数调用(本地x
和调用者的地址)记住的信息会不断堆积,直到操作系统用完内存来存储该信息。
可以实现一个不会溢出“堆栈”的无限递归函数。在足够的优化级别上,许多编译器可以应用优化来删除为尾递归调用记住任何内容所需的内存。例如,考虑以下程序:
int iteration () {
return iteration();
}
当用 编译时gcc -O0
,它变成:
iteration:
.LFB2:
pushq %rbp
.LCFI0:
movq %rsp, %rbp
.LCFI1:
movl $0, %eax
call iteration
leave
ret
但是,当用 编译时gcc -O2
,递归调用被删除:
iteration:
.LFB2:
.p2align 4,,7
.L3:
jmp .L3
这种无限递归的结果是一个简单的无限循环,不会出现“堆栈”溢出。因此,由于允许无限循环,因此允许无限递归。
但是,您的程序不是尾调用优化的候选对象,因为递归调用不是您的函数所做的最后一件事。您的函数仍然有一个return
跟随递归调用的语句。由于递归调用返回后仍有代码需要执行,优化器无法去除递归调用的开销。它必须允许调用正常返回,以便它之后的代码可以执行。因此,您的程序将始终为存储调用代码的返回地址而付出代价。
该标准没有以任何特定术语谈论“无限递归”。我收集了我认为与您的问题相关的内容。
- 允许递归调用函数(C.11 §6.5.2.2 ¶11)
应允许通过任何其他函数链直接或间接调用递归函数。
- 递归进入语句会创建局部变量的新实例(C.11 §6.2.4 ¶5,6,7)
其标识符声明为没有链接且没有存储类说明符 static 的对象具有自动存储持续时间,某些复合文字也是如此。...
对于这样一个没有可变长度数组类型的对象,它的生命周期从进入与其关联的块开始,直到该块的执行以任何方式结束。(进入封闭的块或调用函数会暂停,但不会结束当前块的执行。)如果递归地进入块,则每次都会创建对象的新实例。...
对于这种具有可变长度数组类型的对象,它的生命周期从对象的声明开始,直到程序的执行离开声明的范围。如果递归输入范围,则每次都会创建一个新的对象实例。
该标准在许多地方都谈到了内存分配失败,但从来没有在具有自动存储持续时间的对象的上下文中。标准中未明确定义的任何内容都是未定义的,因此无法分配具有自动存储持续时间的对象的程序具有未定义的行为。这同样适用于具有非常长的函数调用链或太多递归调用的程序。