具有全局定义变量的递归比没有全局定义变量的递归函数获得更多的堆栈.为什么?(跳入C++)
Recursion with global defined variable get more stacks than recursive function without a global defined variable. Why? (jumping into C++)
我正在学习C++ Alex Allain 的"跳入C++"等书籍,并且正在进入递归。 有一个程序示例可以确定我的计算机可以处理多少次递归,直到发生"堆栈溢出错误"或在我的情况下出现"分段错误"。
在使用给定的示例代码之前,我编写了一个简短的程序来自己测试它,它显示了 我的代码可以处理比给定示例代码多一倍以上的递归。我的问题:为什么会这样,因为它几乎是相同的模式 - 函数调用自身并迭代一个整数。
- 为什么在出现错误之前递归次数增加了一倍以上?在我的理解中,两种算法都使用了相同数量的堆栈,在我对它的理解中,它们应该或多或少地消耗相同的数量。
第二,为什么会有分段错误而不是堆栈溢出?
我真的很想了解C ++编程,并希望能够编写使用少量系统资源的"廉价"程序确实是必要的。
我的代码:
#include <iostream>
using namespace std;
int i = 1;
void recurse ()
{
i = i + 1;
cout << "number: " << i << endl;
recurse();
}
int main()
{
recurse();
}
书中的代码示例:
#include <iostream>
using namespace std;
void recurse(int count)
{
cout << count << endl;
recurse(count + 1);
}
int main()
{
recurse(1);
}
我的输出: 在我的系统上输出两个示例的屏幕截图,以查看我的区别 指
使用的编译器Linux g++ -std=c++17 (Arch Linux)
两个版本的代码都具有未定义的行为。编译器可以很容易地证明recurse
函数无休止地递归,并且它最终也会有一个整数溢出。
C++标准明确指出,对具有未定义行为的程序的行为没有约束。你可能会得到堆栈溢出,分段违规,你的硬盘驱动器可能被格式化,核导弹可能会被发射,恶魔可能会从你的鼻子里飞出来,程序可能什么都不做,或者(最糟糕的是)它可能会意外地做你想做的事。
因此,使用具有未定义行为的C++程序测试系统的极限或多或少是没有意义的。
请参阅此处,了解如果删除输出,两个编译器如何在优化下转换代码:
https://godbolt.org/z/Lqln9n
请注意clang
如何看到未定义的行为,并将其替换为"不执行任何操作并返回"。您不会在该编译器上崩溃,该程序将什么都不做!MSVC将无限递归优化为一个平凡的无限循环(loop: jmp loop
)。它将永远重复该一条指令,而不是获得警告中提到的堆栈溢出。
使用混合输出,编译器可能会再次执行不同的操作。但是在每种情况下,除了"编译器决定这样做"之外,您从运行程序中没有得到有意义的答案。您可以检查生成的汇编代码(见上文)以查看编译器执行的操作,然后使用它来解释您看到的行为。但我必须重复一遍,给定的C++代码没有定义的行为,可能会做任何事情。
我同意@Bathsheba的观点,这是一件非常愚蠢和/或糟糕的事情。
但是要回答你的问题:为什么你的代码可以处理比书本代码更多的递归,是因为书本代码有一个局部变量,这反过来意味着它们每次调用使用的堆栈空间比你的代码多。在这里查看堆栈框架的一些解释 - 无需了解所有内容,主要只需查看图片即可获得想法。
然而,这也凸显了这是一个多么愚蠢的练习:你的代码在崩溃之前可以处理多少次递归调用,取决于一大堆参数 - 有些是你控制的,但也有很多你不控制。因此,无论您最终得到什么数字,都没有任何意义。
更不用说,如果您的编译器启用了尾部调用递归优化,那么它将简单地为每个递归调用重用相同的堆栈帧,然后答案是无限的(或整数溢出)。
最简单的解释是,recurse(int count)
的每个调用都必须有自己的count
变量实例,而所有对recurse()
调用都使用i
的单个全局实例。在前一种情况下,count
的私有副本必须存储在某个地方,可能在本地堆栈帧中。
但是,这种解释非常脆弱,因为您的示例非常简单,以至于编译器很可能已经认为它可以在寄存器中传递count
并用简单的循环替换尾递归。这确实是编译器优化的主题...
烧掉书。
你需要一些recurse
的东西来阻止递归。类似的东西
if (count < 10){
recurse(count + 1);
}
从技术上讲,您不做类似事情的行为是不确定的,因为最终int
会溢出。事实上,您在所有执行路径上都有未定义的行为,这意味着不可能对错误说太多:一个积极的编译器将编译你的代码以int main(){}
因为它允许假设未定义的行为不会发生。具有普通优化设置的编译器很可能会将递归展开到无限循环。
禁止编译器优化后,您将遇到运行时约束,例如在达到int
限制之前堆栈溢出。这可能表现为您观察到的分段错误,或者来自C++运行时库/操作系统的其他一些发布。C++标准没有指定应产生的错误。
- 在头文件中定义变量不会出错
- 如何使用 SFINAE 在方法调用中有条件地定义变量?
- 在命名空间中定义变量
- 是否可以在C++中基于程序集输出(.dll或.exe)定义变量
- 用不同类型重新定义变量
- 具有全局定义变量的递归比没有全局定义变量的递归函数获得更多的堆栈.为什么?(跳入C++)
- 定义变量类型
- 在命名空间中定义变量,但在测试中获取空值
- cmake没有定义变量
- C 中的继承:在亲子类中定义变量
- 在同一函数中定义变量及其静态等效项
- 如何在 Tensorflow C++ 中定义变量的自定义有状态 Op 保存值
- 如何将 int 和 int* 传递到函数中以定义变量
- 在定义变量时调用类函数
- 定义变量与模板
- 定义变量类型
- 为什么您可以在 switch 语句中的"默认"下定义变量,而不能在"case"下定义变量
- C 如何基于另一个变量和某些算术定义变量的值
- C 可以指向具有定义变量的函数
- C 定义变量设置值