Code Fix

上級

Segmentation fault(再帰関数の終了条件忘れによるスタックオーバーフロー)

再帰関数に正しい終了条件(ベースケース)が無いと、関数呼び出しのたびにスタック領域が積み上がり続け、上限に達した時点でクラッシュします。

エラーメッセージの読み方

Segmentation fault
(エラーメッセージなし)
これはプログラム自身の出力ではありません。プログラムは何も言わずに落ちています
Segmentation fault
シェルが検知した異常終了の種類 — OSが「不正なメモリアクセス」などを検出しプロセスを強制終了させました
見つけ方
このメッセージだけでは行番号すら分かりません。原因はコードを読んで特定するしかありません

このエラーが出る典型パターン

パターン1

 1  #include <stdio.h>
 2  
 3  int factorial(int n) {
 4      if (0       ) {
                ^
 5          return 1;
 6      }
 7      return n * factorial(n - 1);
 8  }
 9  
10  int main(void) {
11      printf("%d\n", factorial(5));
12      return 0;
13  }
Segmentation fault

if (0)は常に偽なので、終了条件が一度も成立せずn-1、n-2……と再帰が終わらなくなります。関数呼び出しのたびにスタックが積み上がり、上限に達するとクラッシュします。

直し方: 0 を n <= 1 にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  #include <stdio.h>
 2  
 3  int sumTo(int n) {
 4      if (0       ) {
                ^
 5          return 0;
 6      }
 7      return n + sumTo(n - 1);
 8  }
 9  
10  int main(void) {
11      printf("%d\n", sumTo(5));
12      return 0;
13  }
Segmentation fault

ベースケースの条件を書き忘れる(あるいは常に偽になる条件を書いてしまう)と、再帰は自然には止まりません。

直し方: 0 を n <= 0 にします。

この問題を解いてみる →

パターン3

 1  #include <stdio.h>
 2  
 3  int power(int base, int exp) {
 4      if (0         ) {
                 ^
 5          return 1;
 6      }
 7      return base * power(base, exp - 1);
 8  }
 9  
10  int main(void) {
11      printf("%d\n", power(2, 4));
12      return 0;
13  }
Segmentation fault

呼び出しが数千〜数万回に達するまで正常に動いているように見えることもありますが、スタック領域はヒープよりずっと小さいため、いずれ必ず上限に達してクラッシュします。

直し方: 0 を exp <= 0 にします。

この問題を解いてみる →

パターン4

 1  #include <stdio.h>
 2  
 3  int countdown(int n) {
 4      if (0       ) {
                ^
 5          return 0;
 6      }
 7      return countdown(n - 1);
 8  }
 9  
10  int main(void) {
11      printf("%d\n", countdown(5));
12      return 0;
13  }
Segmentation fault

if (0)は常に偽なので終了条件が成立せず、再帰が終わらなくなります。

直し方: 0 を n <= 0 にします。

この問題を解いてみる →

パターン5

 1  #include <stdio.h>
 2  
 3  int fib(int n) {
 4      if (0       ) {
                ^
 5          return n;
 6      }
 7      return fib(n - 1) + fib(n - 2);
 8  }
 9  
10  int main(void) {
11      printf("%d\n", fib(5));
12      return 0;
13  }
Segmentation fault

フィボナッチ数列の再帰でも同様です。ベースケースが成立しないと呼び出しが終わりません。

直し方: 0 を n <= 1 にします。

この問題を解いてみる →

よくある誤解

無限ループと違ってすぐには気づきにくいのが特徴です。呼び出しが数千〜数万回に達するまで正常に動いているように見えることもありますが、スタック領域はヒープよりずっと小さいため、いずれ必ず上限に達します。

実務での勘所

再帰のたびにスタックへ積まれるのは、ローカル変数だけでなく戻り先のアドレスや保存されたレジスタの値も含まれ、1回の呼び出しで数十〜数百バイトを消費します。Linuxの既定のスタックサイズは多くの場合8MB程度で、ヒープに比べるとかなり小さく、ulimit -sコマンドで確認・変更できます。末尾再帰(関数の最後で自分自身を呼ぶ形)であれば理論上はループに変換できますが、Cの規格はこの最適化を保証していないため、深い再帰が必要な処理は最初からループで書くほうが安全です。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)