C++プログラミングとコンピュータシステムアーキテクチャの初心者である私は、まだC++の基礎を学んでいます。昨日再帰関数について読んだので、自分で書くことにしました。これが私が書いたものです:(非常に基本的な)
int returnZero(int anyNumber) {
if(anyNumber == 0)
return 0;
else {
anyNumber--;
return returnZero(anyNumber);
}
}
そして私がこれを行うとき:int zero1 = returnZero(4793);スタックオーバーフローが発生しますが、値4792をパラメータとして渡しても、オーバーフローは発生しません。
理由について何かアイデアはありますか?
再帰的に含む関数を呼び出すときはいつでも、戻りアドレスと多くの場合引数が 呼び出しスタック にプッシュされます。スタックは有限であるため、再帰が深すぎると、最終的にスタックスペースが不足します。
私が驚いたのは、スタックをオーバーフローさせるのにマシンで4793回の呼び出ししかかからないことです。これはかなり小さなスタックです。比較として、私のコンピューターで同じコードを実行するには、プログラムがクラッシュするまでに最大100倍の呼び出しが必要です。
スタックのサイズは構成可能です。 Unixでは、コマンドはulimit -s
です。
関数が tail-recursive であるとすると、一部のコンパイラーは、再帰呼び出しをジャンプに変えることで、再帰呼び出しを最適化できる可能性があります。一部のコンパイラは、例をさらに詳しく説明します。最大の最適化を求められると、gcc 4.7.2
は関数全体を次のように変換します。
int returnZero(int anyNumber) {
return 0;
}
これには、正確に2つのアセンブリ命令が必要です。
_returnZero:
xorl %eax, %eax
ret
かなりきちんと。
システムのコールスタックのサイズ制限に達しただけです。それが起こっていることです。何らかの理由で、システムのスタックは小さく、4793関数呼び出しの深さはかなり小さいです。
スタックのサイズには制限があるため、4793
の呼び出しを行うと、4792
がちょうど下にある間に制限に達します。各関数呼び出しは、ハウスキーピングとおそらく引数のためにスタック上のスペースを使用します。
このページでは、再帰的な関数呼び出し中にスタックがどのように見えるかを 例 示します。
「無限の」再帰、つまり自然に小さい(ish)数に制限されない再帰呼び出しは、この効果があります。制限がどこに行くかは、OS、関数が呼び出される環境(コンパイラ、再帰関数を呼び出す関数など)によって異なります。
再帰関数を呼び出す関数にint x[10];
などの別の変数を追加すると、それをクラッシュさせるのに必要な数が変わります(おそらく約5程度)。
別のコンパイラ(または最適化がオンになっているなど、別のコンパイラ設定)でコンパイルすると、おそらく再び変更されます。
私の推測では、スタックは4792エントリを収めるのに十分な大きさです-今日。明日か次の日か、その数は違うかもしれません。再帰的プログラミングは危険な場合があり、この例はその理由を示しています。再帰がこれほど深くならないようにします。そうしないと、「悪い」ことが起こる可能性があります。
再帰を使用すると、SuperDigitを実現できます。
public class SuperDigit
{
static int sum = 0;
int main()
{
int n = 8596854;
cout<<getSum(n);
}
int getSum(int n){
sum=0;
while (n > 0) {
int rem;
rem = n % 10;
sum = sum + rem;
n = n / 10;
getSum(n);
}
return sum;
}
}