C言語のダイナミックループを再帰と解析木で理解する

C言語のダイナミックループを再帰と解析木で理解する

ループの段数が変わる処理を、トップダウン解析とデータ追跡で読める形にする

Copyright © 2026 LWP 山中 一弘

本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。

記事要約

C言語で、ループの段数が実行時の条件によって変わる処理を書くと、コードは短いのに読み取りが急に難しくなります。典型例が、再帰呼び出しとループを組み合わせて、2重、3重、4重のような多重ループを動的に作る処理です。

このような処理は、単にコードを上から読んでも、どの階層でどの値が決まり、どこで次の階層へ進み、どこで戻ってくるのかが見えにくくなります。特に、再帰呼び出しがループの中にある場合、処理の流れは直線ではなく木構造になります。

この記事では、ダイナミックループを「再帰でループの階層を進め、配列に各階層の値を保存する処理」として整理します。そのうえで、トップダウン解析、デバッグプリントの位置、番兵、非再帰版の見方を、読者が自分で追える形に直します。

本記事の対象とゴール

想定読者

  • C言語で再帰関数を読んでいるが、ループ中の再帰呼び出しで混乱しやすい人

  • 多重ループ、組み合わせ生成、探索処理を、固定段数ではなく可変段数で扱いたい人

  • 再帰のデバッグプリントをどこに入れればよいか迷っている人

  • コードの動作を、表、木、変数の変化として説明できるようにしたい人

本記事で得られること

  1. ダイナミックループを、可変段数の多重ループとして説明できる。

  2. 再帰関数を、呼び出し階層とループ範囲の組み合わせとして解析できる。

  3. デバッグプリントを入れる位置を、入口、再帰前、再帰後、終了前に分けて判断できる。

  4. 解析結果を、コードそのものではなく、木、表、言葉で説明する必要性を理解できる。

本記事で扱わないこと

  • 元教材の正確なCソースコード全体の再掲

  • 組み合わせ生成アルゴリズムの網羅的な分類

  • C言語の再帰最適化やスタック使用量の詳細

  • 画像内コードや図の完全な復元

先に結論

ダイナミックループは、ループの段数をコード上に固定せず、再帰呼び出しによって階層を1つずつ進める考え方です。

固定の3重ループなら、コードには for が3個並びます。しかし、段数が実行時に決まる場合、for を必要な数だけ手で書くことはできません。そこで、1つの関数が「今の階層のループ」を担当し、次の階層は同じ関数を再帰呼び出しすることで表します。

この処理を読むときは、コードだけを見るのではなく、次の3つを分けて追います。

  1. 今の階層を表す値。

  2. その階層で回すループ範囲。

  3. 各階層で選ばれた値を保存する配列。

再帰は、上から下へ呼び出しが増えるときだけでなく、下から上へ戻ってくるときも重要です。そのため、解析では、関数入口、再帰前、再帰後、終了前のどこを観察しているのかを明確にします。

1. ダイナミックループとは何か

この章では、ダイナミックループを「ループの段数が変わる多重ループ」として定義します。先に固定段数のループと比べると、なぜ再帰が必要になるのかが見えます。

1.1 固定段数の多重ループ

3つの値を選ぶ処理であれば、固定の3重ループとして書けます。

for (i0 = 5; i0 <= 7; i0++) {
    for (i1 = 2; i1 <= 2; i1++) {
        for (i2 = 1; i2 <= 3; i2++) {
            action(i0, i1, i2);
        }
    }
}

この書き方は読みやすい反面、段数が固定です。2段なら2重ループ、4段なら4重ループを書き直す必要があります。

1.2 段数が変わるとコードに書けない

問題は、必要な段数がデータによって変わる場合です。

たとえば、組み合わせる要素の個数が2個なら2重、3個なら3重、5個なら5重のループが必要になります。これをすべて手で書くと、同じ形のコードが段数分だけ増えます。

ダイナミックループでは、1つの関数に「今の階層のループ」を担当させます。そして、次の階層が必要なら、同じ関数を再帰呼び出しします。

考え方だけを擬似コードで書くと、次の形になります。

void loopfun(int k)
{
    int i;
    for (i = start[k]; i <= end[k]; i++) {
        r[k] = i;
        if (k == last_level) {
            action(r);
        } else {
            loopfun(k + 1);
        }
    }
}

このコードで重要なのは、k がループの階層を表し、r[k] がその階層で選ばれた値を保存する、という点です。

2. 番兵と終了条件を分けて考える

この章では、ダイナミックループを読む前提として、データの終わりをどう表すかを整理します。元素材では、C言語の番兵と、VBAでの CVErr による番兵の話が接続されています。

2.1 番兵は終わりをデータ側に埋め込む工夫である

番兵とは、データの最後を表す特別な値です。

C言語の文字列であれば、文字配列の末尾に '\0' を置きます。多くの文字列関数は、この '\0' までを文字列として扱います。

char s[] = { 'A', 'B', 'C', '\0' };

この考え方を使うと、別途「要素数」を持たなくても、データを順に読んでいき、番兵に到達したところで終わりだと判断できます。

2.2 C言語では番兵の値に苦労する

C言語では、普通の値と番兵を区別する値を選ぶ必要があります。これは簡単なようで難しい問題です。

たとえば getchar は、文字そのものではなく int を返します。これは、普通の文字コードと、入力終了を表す EOF を区別するためです。

文字は通常の文字コード範囲にあります。一方で、EOF はその範囲と区別できる値として扱います。つまり、普通のデータと終端を同じ型の中で区別するために、戻り値の型や値の範囲を工夫しているわけです。

2.3 VBAではCVErrを番兵に使える

VBAでは、配列やVariantを使う場面で、CVErr によって作ったエラー値を番兵として使う方法があります。

通常の値とエラー値は性質が違うため、データ要素と終端マーカーを区別しやすくなります。

この記事の主題はC言語のダイナミックループですが、番兵の考え方は共通です。データの終わりをどこで判断するかを明確にすると、ループと再帰の終了条件も読みやすくなります。

3. 再帰版ダイナミックループをトップダウンで読む

この章では、再帰版のダイナミックループを読む方法を整理します。鍵は、呼び出しを木として見ることです。

3.1 関数呼び出しを階層として読む

元素材では、lf(0, 5~7) のような表記で、関数呼び出しを説明しています。

ここで lf は loopfun の省略、最初の数値はループの階層、後ろの範囲はその階層で回す値の範囲です。

lf(0, 5~7)

これは、階層0で、5から7までを回す呼び出しだと読めます。

この呼び出しからは、5、6、7のそれぞれを選んだ状態で、次の階層へ進む枝が伸びます。

lf(0, 5~7)
  ├─ r[0] = 5 -> lf(1, ...)
  ├─ r[0] = 6 -> lf(1, ...)
  └─ r[0] = 7 -> lf(1, ...)

ここで大事なのは、r[0] が階層0で選ばれた値を保持することです。次の階層へ進むと、今度は r[1] が更新されます。

3.2 トップダウン解析は呼び出し元から読む

トップダウン解析では、呼び出し元から順に、どのように再帰の木が構築されるかを見ます。

たとえば、lf(1, 2~2) が呼ばれたとき、すでに r[0] には上位階層で選ばれた値が入っています。この階層の関心は r[1] です。

lf(0, 5~7)
  └─ r[0] = 5
      └─ lf(1, 2~2)
          └─ r[1] = 2

このように、階層ごとに「どの配列要素が決まるのか」を分けると、再帰の木が見やすくなります。

3.3 配列rは選択済みの経路である

r 配列は、単なる作業用配列ではありません。再帰の各階層で選ばれた値を保存する、経路の記録です。

階層0で5を選び、階層1で2を選び、階層2で1を選んだなら、r は次の状態になります。

r[0] = 5
r[1] = 2
r[2] = 1

この状態で最下層に到達したとき、action(r) のような処理を実行すれば、選択済みの組み合わせを使えます。

4. 再帰のデバッグプリントは位置に意味がある

この章では、再帰関数を調べるときに、どこへデバッグプリントを入れるべきかを整理します。再帰では、出力位置を間違えると、何を観察しているのかが分からなくなります。

4.1 観察位置は4つある

再帰関数の中で観察したい位置は、大きく4つあります。

A: 関数に入った直後
B: 再帰呼び出しの直前
C: 再帰呼び出しから戻った直後
D: 関数を抜ける直前

擬似コードで置くと、次の位置です。

void loopfun(int k)
{
    print("A", k, r);
    for (...) {
        r[k] = i;
        print("B", k, r);
        loopfun(k + 1);
        print("C", k, r);
    }
    print("D", k, r);
}

Aは、関数が呼ばれたときの状態を見る位置です。Bは、子の再帰へ進む直前の状態を見る位置です。Cは、子の再帰から戻った直後の状態を見る位置です。Dは、その関数が終わる直前の状態を見る位置です。

4.2 まず見るならAとCでよい

元素材では、第一候補としてAとCの位置が挙げられています。

Aを見ると、その階層がどの状態で始まったのかが分かります。Cを見ると、子の再帰から戻ってきたあと、どの呼び出し元へ戻ったのかが分かります。

再帰で分かりにくいのは、呼び出す前よりも、戻ってきたあとです。コード上は loopfun(k + 1); の次の行に戻るだけですが、実行時には、どの階層のどのループ反復へ戻ったのかを見失いやすくなります。

そのため、再帰を説明する目的では、Cの位置が特に重要です。

4.3 BとDは必要に応じて使う

Bは、再帰呼び出しの直前を観察する位置です。Aと意味が近い場合もありますが、r[k] に値を入れた直後を見たい場合には有効です。

Dは、関数を抜ける直前を観察する位置です。戻り値に意味がある関数や、終了直前に状態を戻す関数では重要になります。ただし、今回のように選択配列を追う処理では、必須ではない場合もあります。

デバッグプリントは多ければよいわけではありません。何を見たいのかに応じて、観察位置を選ぶことが重要です。

5. 解析的手法と説明用モデルを分ける

この章では、コードを追うことと、読者に説明できる形に直すことを分けます。動いたことを確認するだけでは、理解したことにはなりません。

5.1 解析的手法はコードとデータを追う

解析的手法とは、コードの実行順とデータの変化を追い、ロジックが間違いなく動くことを確認する方法です。

ダイナミックループであれば、次を表にします。

階層 k
ループ変数 i
r[0], r[1], r[2] の状態
次に呼ばれる関数
actionが実行されるタイミング

この方法を使うと、再帰がどのように進み、どこで組み合わせが完成するかを検証できます。

5.2 説明には木や表が必要になる

ただし、コードとデータを追っただけでは、説明としては不十分です。

解析した内容を元に、ロジックがどのような考えで動くのかを、木、表、言葉で構築し直す必要があります。

たとえば、再帰呼び出しは木として表すと分かりやすくなります。

階層0: 5, 6, 7を選ぶ
  階層1: 2を選ぶ
    階層2: 1, 2, 3を選ぶ
      action

このように表すと、コードを読んでいない人にも、各階層が1つのループに対応していることが伝わります。

6. 非再帰版ダイナミックループの見方

この章では、元投稿の後半で扱われている非再帰版の考え方を整理します。再帰を使わなくても、状態配列と制御構造を使えば、ダイナミックループに近い処理を表せます。

6.1 外側の永久ループは戻り先として働く

非再帰版では、2重ループに見えても、実質的には1つの変数を進めながら状態を更新している場合があります。

元素材では、外側のループは、内側の条件が満たされたあとに戻る先として働くと整理されています。つまり、外側の永久ループが、goto 的な役割を持ちます。

考え方を擬似コードで表すと、次のようになります。

for (;;) {
    action(r);
    i = 0;
    for (;;) {
        if (r[i] can be advanced) {
            advance r[i];
            break;
        }
        reset r[i];
        i++;
        if (i == n) {
            return;
        }
    }
}

この形では、内側のループで r 配列のどこかが変化すると、外側のループへ戻ります。外側では action を実行し、次の探索へ入ります。

6.2 breakの意味を展開して読む

この構造で大事なのは、break を単なる脱出と見ないことです。

内側のループで状態が1つ進んだとき、break によって外側へ戻ります。外側へ戻ると、次の3つが起きます。

  1. action を呼び出す。

  2. ループ変数を初期化する。

  3. 内側の探索へ戻る。

つまり、break は単独の動作ではなく、外側ループの先頭へ戻ることで一連の処理を引き起こしています。

このように読むと、非再帰版のダイナミックループも、状態配列を少しずつ進める処理として理解できます。

7. 学習として大事なのは発表できる形にすること

この章では、元投稿全体の教育的な論点を整理します。塾や学習会では、コードが動くかどうかだけでなく、どのように考えたかを説明できることが重要です。

7.1 分からなかった点を言語化する

再帰やダイナミックループは、読める人には短いコードに見えます。しかし、初学者にとっては、どこで次の階層へ進み、どこへ戻ってくるのかが見えません。

そのため、学習では、分かったことだけでなく、分からなかった点を言語化することが重要です。

たとえば、次の問いを立てます。

どの変数が階層を表しているか。
どの配列が選択済みの値を持っているか。
どこで次の階層へ進むか。
どこで呼び出し元へ戻るか。
actionはどの状態で呼ばれるか。

この問いに答えられるようになると、コードの追跡が説明へ変わります。

7.2 解析には2つの方向がある

解析手法には、大きく2つの方向があります。

1つ目は、コードにデータの変化を書き込む方法です。各行で i や r がどう変わるかを追記します。

2つ目は、データを中心にして、そのデータを変化させたコードを書く方法です。r[0]、r[1]、r[2] の値がどう変わったかを先に表にし、そこに対応するコード位置を書き込みます。

どちらも有効です。大事なのは、コードだけを眺めるのではなく、コードとデータの対応を自分の手で作ることです。

まとめ

ダイナミックループは、段数が固定されていない多重ループを扱うための考え方です。

再帰版では、1つの関数が今の階層のループを担当し、次の階層を同じ関数の再帰呼び出しで表します。k のような値が階層を表し、r[k] のような配列要素が、その階層で選ばれた値を保存します。

この処理を読むときは、コードを上から追うだけでは不十分です。呼び出しを木として見て、各階層のループ範囲と配列の状態を追う必要があります。

再帰のデバッグプリントでは、関数入口、再帰前、再帰後、終了前のどこを見ているのかを明確にします。特に、子の再帰から戻った直後は、再帰の流れを理解するうえで重要です。

非再帰版では、外側のループが戻り先として働き、内側のループで状態配列を進める構造として読むと分かりやすくなります。

最終的に大事なのは、コードが動くことを確認するだけではありません。コードとデータの変化を追い、それを木、表、言葉で説明できる形に直すことです。これが、ダイナミックループを本当に理解するための解析的手法です。

出典メモ- 元URL: https://togetter.com/li/1694692

  • 正規化後URL: https://posfie.com/@hoehoe1234/p/BwBNJbL

  • 元タイトル: エンジョイC036回目ダイナミックループ(2020-12-13)

  • 取得日: 2026-06-01

  • 入力元: C:\Users\hoehoe\マイドライブ\LWP記事\03過去記号\エンジョイC\エンジョイC036回目ダイナミックループ(2020-12-13) - Togetter.URL

  • 抽出素材: Togetter/posfie HTMLから取得できた投稿本文25件。画像内のコードや図は完全復元せず、投稿本文から読み取れる範囲をもとに、本文だけで理解できる説明へ再構成した。