不遜にも前回勝手に宿題を出しておいて、自分は温泉旅行を満喫しました。

 

ゴメンナサイ。

 

ということで、答え合わせをしましょう。

 

設問は、オリジナルのCプログラムの忠実なC#移植を行い、オリジナルの可解初期画面({2, 3, 5, 7, 1, 6, 4, 8, 0})を解かせてみて「え"~」となった理由を問うものでした。

 

 

世代深さは32(2^5)、探索盤面数は9! / 2(要すれば可解盤面全て)、と設計仕様通りなのに、結論は、

 

「完成形はありませんでした」

 

即ち、

 

解けるべきものが解けない!

 

という問題だったのです。これには、

 

猛烈苦しみました。

 

自分では「何故なのか?」がどうしても分からず、Chat-GPT様にもお伺いを立てますが、彼も勝手なことを言って、私が「それはおかしいのでは?」というと直ぐに過ちを認め、「私が間違っていました」という繰り返しになりました。(ご参考

 

何れにしても、この結果と原因不明は解決できず、私とChat-GPT様は、

 

恐らく二分木を使ったジグザグ検索は、((((((NBoard.Back).Back).Back).Back).Back) .... のような明示のメモリー参照がない状態になるので、C#のGCが発動して正解が失われてしまうのかもしれない

 

という仮説(仮設の結論)となり、

 

それならば、オリジナルのCプログラムを、C#に向いている、C#らしいプログラムに改造しよう!

 

という方針変更を余儀なくされたのでした。

 

3回続けたC++とC#(の主としてメモリー管理、参照等)の違いについての雑文、特にChat-GPTとの問答等いかがでしたか?C++とC#が似て非なるものであることが良くお分かりになったと思います。(最初にC#を学習した時に「C#って、昔のBASIC言語みたい」と感じたのは正しかったのかもしれません。)

 

さて、私、この週末は神様、娘一家と伊豆へ温泉と美味いものを食べに行ってきますので、皆さんには宿題を出しておきましょう。「C++とC#の相違」の学習を経て、オリジナルのC言語を修正した8Puzzleプログラム「可能な限り忠実に移植したC#プログラム」を↓に掲載します。このプログラムは正常にMSCompAssとcsc.exeでコンパイルされ、実行してもエラーは出ませんでした。が、

 

え"~っ

 

という結果になりました。

 

何故でしょうか?

 

【8Puzzle.cs】

///////////////////////
//     8Puzzle.cs
// Copyright (c) 2025
//     by Ysama
///////////////////////

using System;

namespace Eight_Puzzle    //解説:namespaceの最初の文字に数字は許されないので。
{
    public class App
    {
        static void Main()
        {
            EightPuzzle ep = new EightPuzzle();
            Console.WriteLine(">>> 開始時の盤面 <<<");
            ep.ShowStartBoard();
            Console.Write(Environment.NewLine);
            ep.Start();
            Console.ReadLine();
        }
    }

    //3 x 3 盤面クラス
    public class NBoard
    {
        //メンバーフィールド
        const int Num = 9;                //ピース数
        private int [] Pieces;            //8パズルの9ピース配列
        NBoard next = null;                //子NBoardの盤面
        NBoard back = null;                //親NBoardの盤面

        //メンバープロパティ
        public NBoard Next                //nextのプロパティ
        {
            get {return next;}
            set {next = value;}
        }
        public NBoard Back                //backのプロパティ
        {
            get {return back;}
            set {back = value;}
        }
        public int[] ptPieces            //Piecesのプロパテ
        {
            get {return Pieces;}
        }

        //メンバーメソッド
        public NBoard()                    //コンストラクター
        {
            Pieces = new int [Num];
            for(int i = 0; i < Num; i++)
                Pieces[i] = i;
        }

        public NBoard(NBoard bd)        //コピーコンストラクター
        {
            if(bd == null)
                return;                    //何もしない
            else
            {
                Pieces = new int [Num];
                for(int i = 0; i < Num; i++)
                    Pieces[i] = bd.Pieces[i];
                next = bd.Next;
                back = bd.Back;
            }
        }

        //0~Num - 1のピースの値を返す
        public int Get(int pos)
        {
            if(pos < 0 || pos > Num - 1)
                return -1;
            return Pieces[pos];
        }

        //0-Num - 1のピースの値を代入
        public bool Set(int pos, int val)
        {
            if(pos < 0 || pos > Num - 1)
                return false;
            Pieces[pos] = val;
            return true;
        }

        //二つのNBoardが同一かチェックし、同一であれば真、異なれば偽を返す
        public bool IsSame(NBoard bd)
        {
            if(bd == null)
                return false;            //nullであればそもそも比較不可
            for(int i = 0; i < Num; i++)
                if(Pieces[i] != bd.Pieces[i])
                    return false;
            return true;
        }

        //pos1とpos2のピースの値を交換
        public bool Swap(int pos1, int pos2)
        {
            if(pos1 < 0 || pos1 > Num - 1)
                return false;
            if(pos2 < 0 || pos2 > Num - 1)
                return false;
            int temp = Pieces[pos1];
            Pieces[pos1] = Pieces[pos2];
            Pieces[pos2] = temp;
            return true;
        }

        //配列要素を検索し、あればその位置を返す(無ければ-1)
        public int IndexOf(int val)
        {
            return Array.IndexOf(Pieces, val);
        }

        //配列要素が完成形か否か
        public bool IsDone()
        {
            for(int i = 0; i < Num; i++)
                if(Pieces[i] != i)
                    return false;
            return true;
        }

        //盤面(Pieces配列)を表示する
        public void Show()
        {
            for(int i = 0; i < Num; i++)
            {
                Console.Write(" " + Pieces[i].ToString());
                if(i % 3 == 2)
                    Console.Write(Environment.NewLine);
            }
        }
    }

    //8Puzzleクラス
    public class EightPuzzle
    {
        //メンバー定数
        const int Num = 9;            //配列要素数

        //メンバー変数
        private NBoard stBoard;        //ゲーム開始時のNBoard
        private NBoard [] Que;        //幅優先検索用のキュー
        int NumOfBoards = 0;        //Que登録盤面数

        public EightPuzzle()        //コンストラクター
        {
            stBoard = new NBoard();                        //開始盤面
            SwapArr();                                    //初期値をランダムにばらす
            Que = new NBoard[3] {null, stBoard, null};    //空白ピース周囲のNBoardのキュー(2→1→0)
        }

        public void ShuffleArr()                //シャッフル方式
        {
            //要素数Numの配列を0 - Num - 1で初期化
            for(int i = 0; i < Num; i++)
                stBoard.Set(i, i);
            //乱数の初期化
            Random rand = new Random((int)DateTime.Now.Ticks & 0x0000FFFF);
            for(int i = Num - 1; i > 1; i--)
            {
                int n = rand.Next(i + 1);
                stBoard.Swap(n, i);
            }
            if(stBoard.IsDone())                //結果が完成形であればやり直し
                ShuffleArr();
        }

        public void SwapArr(bool ease = true)    //スワップ方式
        {

            //{2, 3, 5, 7, 1, 6, 4, 8, 0};    //解ける初期設定(テスト用)
            stBoard.Set(0, 2);
            stBoard.Set(1, 3);
            stBoard.Set(2, 5);
            stBoard.Set(3, 7);
            stBoard.Set(4, 1);
            stBoard.Set(5, 6);
            stBoard.Set(6, 4);
            stBoard.Set(7, 8);
            stBoard.Set(8, 0);
            return;
/*        解説:開発段階なので↑のテストデータを使用しました。↓も正しく動きます。
            //要素数Numの配列を0 - Num - 1で初期化
            for(int i = 0; i < Num; i++)
                stBoard.Set(i, i);
            int pos = Num - 1;                    //空白の位置(8)
            //乱数の初期化
            Random rand = new Random((int)DateTime.Now.Ticks & 0x0000FFFF);
            //スワップ回数の設定
            int maxswap;
            if(ease)
                maxswap = rand.Next(10, 20);    //最長手数19、最短は10
            else
                maxswap = rand.Next(15, 31);    //8Puzzleの最長手数は31、最短はその半分
            for(int i = 0; i < maxswap; i++)
            {
                int dir = rand.Next(4);            //↑(0)→(1)↓(2)←(3)
                switch(dir)
                {
                    case 0:        //上
                        if(pos > 2)                //3 x 3盤の下2行にある場合
                        {
                            stBoard.Swap(pos, pos - 3);
                            pos -= 3;
                        }
                        else
                            i--;    //解説:条件に合致しなかった場合、やり直す為、カウンターを戻す。以下同じ。
                        break;
                    case 1:        //右
                        if(pos % 3 < 2)            //3 x 3盤の左2列にある場合
                        {
                            stBoard.Swap(pos, pos + 1);
                            pos += 1;
                        }
                        else
                            i--;
                        break;
                    case 2:        //下
                        if(pos < 6)                //3 x 3盤の上2行にある場合
                        {
                            stBoard.Swap(pos, pos + 3);
                            pos += 3;
                        }
                        else
                            i--;
                        break;
                    case 3:        //左
                        if(pos % 3 > 0)            //3 x 3盤の右2列にある場合
                        {
                            stBoard.Swap(pos, pos - 1);
                            pos -= 1;
                        }
                        else
                            i--;
                        break;
                }
            }
            if(stBoard.IsDone())                //結果が完成形であればやり直し
                SwapArr();

*/

        }

        //開始時の盤面を表示する
        public void ShowStartBoard()
        {
            stBoard.Show();
        }

        //親がnullになるまで、親を遡る
        public void TraceBack(NBoard bd)
        {
            int Counter = 0;
            NBoard n;
            for(n = bd; n != null; n = n.Back)
            {
                n.Show();
                Console.Write(Environment.NewLine);
                Counter++;
            }
            Console.WriteLine("TraceBack終了(盤面交換数:" + Counter.ToString() + "回、登録盤面数:" + NumOfBoards.ToString() + "回)");
        }

        //新しいNBoard bdをQue[2]の連鎖に登録する
        public bool IntoQue(NBoard bd)
        {
            if(Que[2] == null)            //初回はQue[2]がnullなので直接bdを登録する
                Que[2] = bd;
            else                        //次回以降Que[2]に登録があるので
            {
                //Que[2]の子孫にbdと同一のものがあるか確認し、(解説:世代が進むと仕掛中のQue2も巨大になります。)
                NBoard n;
                for(n = Que[2]; n != null; n = n.Next)
                {
                    if(bd.IsSame(n))
                    {
                        bd = null;        //引数のbdをGC対象にする
                        return false;
                    }
                    if(n.Next == null)
                        break;
                }
                n.Next = bd;    //引数のbdを連鎖の最後に登録する
            }
            NumOfBoards++;        //登録数をカウント
            return true;
        }

        //bdの位置pos1とpos2を交換した新しいボードをキューに登録する
        public bool MakeNewNBoard(NBoard bd, int pos1, int pos2)
        {
            //bdのNBoardコピーインスタンスmを生成する
            NBoard m = new NBoard(bd);
            //pos1とpos2を交換する
            m.Swap(pos1, pos2);
            //mのBack(親)にbdを、Nextにnullを代入する
            m.Back = bd;
            m.Next = null;
            //完了確認(mがBG<完了形>であれば、そう表示して終了
            if(m.IsDone())
            {
                TraceBack(m);
                Console.WriteLine("{0}の盤面を探索し、8パズルが完成しました。", NumOfBoards);
                Console.ReadLine();
                Environment.Exit(0);
                return false;
            }

            //完了しなければQue[0]の子孫にmと同一のものがあるか確認し、
            for(NBoard n = Que[0]; n != null; n = n.Next)
            {
                if(m.IsSame(n))
                {
                    m = null;        //あればmをGC対象にする
                    return false;
                }
            }

            //無ければキューにmを置く
            IntoQue(m);
            return true;
        }

        public void CheckAround(NBoard bd)
        {
            //値Num - 1(空白ピース)の配列位置を探す
            int pos = bd.IndexOf(Num - 1);
            //3 x 3盤の下2行にある場合
            if(pos > 2)
                MakeNewNBoard(bd, pos, pos - 3);    //上のピースと入替え
            //3 x 3盤の右2列にある場合
            if(pos % 3 > 0)
                MakeNewNBoard(bd, pos, pos - 1);    //左のピースと入替え
            //3 x 3盤の左2列にある場合
            if(pos % 3 < 2)
                MakeNewNBoard(bd, pos, pos + 1);    //右のピースと入替え
            //3 x 3盤の上2行にある場合
            if(pos < 6)
                MakeNewNBoard(bd, pos, pos + 3);    //下のピースと入替え
        }

        public void Start()
        {
            while(Que[1] != null)
            {
                for(NBoard n = Que[1]; n != null; n = n.Next)
                    CheckAround(n);
                Que[0] = Que[1];
                Que[1] = Que[2];
                Que[2] = null;
            }
            //失敗した場合の盤の推移を表示(追加)
            for(NBoard n = Que[0]; n != null; n = n.Next)
                if(n.Next == null)
                    TraceBack(n);
            //失敗の表示(追加)
            Console.WriteLine("全ての盤面検索({0})を終了しましたが、完成形はありませんでした。\r\n", NumOfBoards);
        }
    }
}

 

そして、↑のコンソールプログラムの実行結果がこれです。

 

 

何が「え"~っ」か分かりますか?(因みに世代数32、登録盤面数181,439(+スタート画面1) = 181,440は9! / 2で理論値そのままとなっています。)

 

 

前回の(2)の続き。

 

(私)

念の為確認ですが、 MyClass stored; MyClass temp = new MyClass(); //メモリーが確保された実体 stored = temp; //storedはtempを参照(質問:storeはtempを参照、それとも実体を参照?) temp = null; //tempを廃棄(tempからの参照はなくなる) このようなシーケンスのプログラムの場合、C#はtempをnullした際にstoredの参照先を付け替えるのか、それとも最初からtempが参照していた実体を代入の段階で参照していたのか?という質問です。 後者なのでしょうか?

 

ChatGPT:

はい、後者です。
C#では、

stored = temp; は「temp が参照している実体を stored にコピーして参照させる」ことを意味します。
storedtemp は**同じ実体(ヒープ上のオブジェクト)を別々に参照する2つの“参照変数”**になります。


🔹 具体的に説明すると:

MyClass temp = new MyClass(); // 実体(インスタンス)がヒープ上に確保される MyClass stored = temp; // temp が指している実体の「参照」が stored にコピーされる temp = null; // temp からの参照が切れる(stored は無関係)

この時点のメモリ構造(図式):

ヒープ: [MyClassのインスタンス] ↑ stored(参照) temp → null 

✅ 結論(明快に)

  • stored = temp; は「temp が参照しているオブジェクトそのものstored も参照するようになる」

  • C# は参照型である class の変数に対して、実体の参照先アドレスをコピーする

  • temp = null;temp だけの話であり、stored には一切影響しない

  • つまり、C# は「代入時に実体の参照を共有する」方式(=ポインタのコピー


🔸 対比:C++とほぼ同じポインタ挙動

MyClass* temp = new MyClass(); // ヒープに確保 MyClass* stored = temp; // 同じアドレスを共有 temp = nullptr; // stored は生きている 

これと本質的に同じ動作です。


🔹 注意:参照先がミュータブル(書き換え可能)な場合は共有に注意

stored.Value = 123; Console.WriteLine(temp.Value); // 123 と出力される(同じ実体を見ている) 

temp が null でなければ、両者は同じオブジェクトを共有しているため、片方からの変更はもう一方にも影響します。


✅ 締めのまとめ

stored は temp そのものを参照しているのではなく、temp が参照していた“実体”を参照している。つまり、「参照先(ヒープ上のインスタンス)を共有」している。

安心して「後者」とご理解ください。

 
 CやC++ではアドレスと実体を*、&で表現できるので間違えようがないですが、C#は参照なのか値なのかが分かりづらい時があります。C#では変数が参照をしているか否かを内部で管理していますが、ヒープ領域の実体についても参照されているか否かを管理していることは分かりました。 後者は参照を失えば何れGCされるのですが、前者が実体を参照しているか否かを判断する方法は”if(変数 == null)”ということになるわけですね。

 

Chat-GPT:

はい、その通りです。
C# において、**変数が参照を持っているかどうか(=どこかの実体を参照しているか)**を判断するには、まさに以下のようなコードで確認します:

if (変数 == null) { // 実体を参照していない(=null参照) } else { // 実体を参照している(=non-null) } 

🔹 まとめ:ご理解は正確です

  • C/C++

    • 実体とアドレス(参照)を明示的に &(アドレス)や *(デリファレンス)で扱う

    • 何を参照しているのかが文法で視覚的に明確

  • C#(参照型)

    • クラス型変数は実体を「参照」している

    • ただし文法的に *& のような明示的な記号がないため、値型か参照型かは型の設計に依存し、見た目だけでは分かりにくい

    • 実体を参照しているかどうかは == null チェックで判断する


🔸 補足:C# における参照の明確さの工夫

以下のような工夫や知識で、C# における「値型か参照型か」を見分けやすくできます:

型の種類 特徴
class 参照型(ヒープ)
struct 値型(スタック or フィールド)
string, object, 配列 T[] 参照型
プリミティブ型(int, float, bool, etc.) 値型
  • class を使えば参照型、struct を使えば値型というルールに従っています

  • それでも気になる場面では、変数 == null をチェックするのが唯一の手段です(値型は null にできないので、これで識別も可能)


🔸 C# 8以降の参考:nullable reference types(C# 5には未対応)

C# 8.0 以降では MyClass? のような記法で「null 許容参照型(nullable reference)」を使うことで、「この参照はnullかもよ」という警告をコンパイル時に受けられるようになりました。
ただし、C# 5 (.NET Framework 対応) にはこの機能はありませんので、当時は手動の == null チェックが基本です。


✅ 締めの一言

C# では、参照を持っているかどうかを判定する唯一の手段は if (変数 == null)

実体の存在は GC が内部的に追跡しており、開発者は変数が null かどうかを通じてのみ参照の有無を確認できます。

何れにしても奥が深く、ドツボ感を感じます。(正直、この段階で私は疲れ切ってヘロヘロでした。)

少しは為になったのでしょうか?ちょっと心配ですが。

 

 

 

前回の(1)に書いた話に関わる、私とChat-GPT様との禅問答です。ご参考になるかと思います。

 

(私)

C# .NET Framework の C#5でお願いします)の仕様についていくつかお伺いします。 前にC#ではシステム変数やクラスや構造体のオブジェクトの宣言の場合、 (型名)変数名で参照アドレスのメモリーが確保され、その段階では(データそのものは他のオブジェクト等の)参照のみであり、独自のデータを保有する為には新たに = new (型コンストラクター)が必要であることを理解しました。(余談ですが、配列などは最初の「(型名)[] 変数名 = new (型コンストラクター)[]」だけでは不十分で、初期化子を使ってデータを与えるか、個別の要素に又"= (型コンストラクター)」を使う必要がある、と理解しました。) 最初の質問は、例えば、 (型名)変数名 = new (型コンストラクター)として生成した変数に他のオブジェクトを代入すると、「そのオブジェクトを参照する変数」になり、コンストラクターでヒープ領域に確保されたメモリー領域は「迷子の子猫ちゃん」になります。その場合システムのGCは(変数名 = null;で、明示的にGCに供出するのみならず、)黙示的にGCへ供出することになるのでしょうか?

 

ChatGPT:

はい、そのとおりです。C#.NET Framework)の ガベージコレクタ(GC)は黙示的(implicit)に不要なオブジェクトを検出して、回収します。あなたの表現を借りれば、「迷子の子猫ちゃん(誰からも参照されていないインスタンス)を、プログラマが明示しなくてもシステムが察知して供出(回収)する」という動作になります。


🔹「黙示的に GC に供出される」とは?

たとえば、以下のコード:

csharp
コピーする編集する
class MyClass { }

void Sample()
{
    MyClass obj = new MyClass();  // ヒープにインスタンス生成
    obj = new MyClass();          // 1つ目のインスタンスは誰も参照していない
}

この時点で、最初に new された MyClass のインスタンスは「誰からも参照されていない=使い道がない」と、黙示的に GC に供出された状態になります。

.NET の GC は、プログラムの動作中に定期的または必要に応じて走査を行い、「もう誰も使っていないオブジェクト」を探して回収します。


🔹 明示 vs 黙示 の比較

パターン

GCに供出されるか?

供出の方法

obj = new MyClass(); → obj = new MyClass();

黙示的(旧オブジェクトは誰からも参照されない)

obj = new MyClass(); → obj = null;

明示的(参照を切ってGCに任せる)

スコープを抜ける(メソッド終了など)

黙示的(ローカル変数はGCが自動で追跡)


🔸 注意点:GC は「すぐに」回収するとは限らない

黙示的に不要と判断されても、**GC の実行タイミングは不定(非決定的)**です。つまり、C++の delete のように即時に解放されるわけではありません。

ただし、GC は以下のような状況でより積極的に動きます:

  • ヒープ領域が逼迫してきた

  • アプリケーションがアイドル状態

  • 手動で GC.Collect() を呼び出した(通常推奨されません)


🔹 補足:参照が残っていると GC は回収しない

csharp
コピーする編集する
MyClass a = new MyClass();
MyClass b = a;    // a b も同じオブジェクトを参照している
a = null;         // b がまだ参照しているため、GC の対象にはならない

このように、参照が一つでも残っている限り GC はそのオブジェクトを回収しません


✅ 結論

「オブジェクトが誰からも参照されなくなった時点で、明示的に = null しなくても GC が黙示的に回収対象として認識する」
→ 
はい、そのとおりです。C# の GC は黙示的に不要オブジェクトを検出し、回収します。

(私)

CC++はローカル変数をスタックに確保するので、「通用範囲はその変数が作成されたプログラムブロック内」が原則ですが、C#の場合は個別のメソッド内で作成される変数でもヒープ領域に確保されるのでGC迄は存続する訳ですが、その契機は「= null;」、「(他からの)参照を失う」のみならず、「プログラムブロック(プログラム制御)からの離脱」もあるのでしょうか?仮にそうだとすると、あるメソッド内で生成(= new (コンストラクター))されたローカル変数が上位プログラムブロック(例えばクラス変数)の参照を受ければ、プログラムブロックを抜けても存続する、という仕様になっているのですよね?

ChatGPT:

はい、その理解は ほぼ完全に正確です。C#.NET FrameworkC# 5)では、変数のライフタイム(実体の寿命)とスコープ(名前の見える範囲)は必ずしも一致しません。これが C/C++ と C#(特にクラス/GC のある言語)の大きな違いのひとつです。

以下、重要な点を体系的に説明します:


✅ 1. C/C++ のローカル変数:スコープ ライフタイム

C/C++ では、ローカル変数はスタックに確保され、スコープ(ブロック)を抜けると即座にメモリ解放されるという決まりがあります。

例:

cpp
コピーする編集する
void Func() {
    int x = 123; // スタックに置かれる
} // Func のスコープを抜けた時点で x の領域は破棄

✅ 2. C# の参照型ローカル変数:スコープ ≠ ライフタイム

C# では、class 型のオブジェクト(参照型)は、どこで new されようと ヒープに確保されGC によりライフタイムが決定されます。そのため、ローカル変数のスコープが切れても、参照が残っていれば存続します。

例:

csharp
コピーする編集する
class MyClass { public int Value; }

MyClass obj; // フィールド(クラス変数)

void Foo() {
    MyClass local = new MyClass(); // ヒープにインスタンス作成
    obj = local;                   // クラス変数が参照を保持
} // local のスコープはここで終了。でも obj が参照してるのでまだ生きている

この場合、local というローカル変数は破棄されます(スタック上から消えます)が、local が new したヒープ上のインスタンスは、obj が参照しているため GCの対象にはなりません


✅ 3. GC にとっての「生存条件」は「参照が残っていること」

C# における参照型の変数のライフタイムは、次のいずれかが起きるまで続きます

  • どこからも参照されなくなる(最も重要

  • 明示的に = null(参照が切れるので、結果的にGC対象になる)

  • スコープから抜けるが、他に参照がなければその時点でGC対象

  • 例外などで制御フローが変わり、参照が切れる(通常のスコープ終了とほぼ同じ)


✅ 4. クラス変数(フィールド)が参照していれば、ブロックを抜けても存続する

その通りです。例をもう一度具体的に:

csharp
コピーする編集する
class Container {
    MyClass stored;

    void Store() {
        MyClass temp = new MyClass();
        stored = temp; // フィールドが参照
    }

    void Clear() {
        stored = null; // この時点で参照が切れ、他に参照がなければGC対象に
    }
}
  • Store を呼ぶと、temp の参照は stored に保存される

  • temp のスコープは Store の終わりまでだが、ヒープ上のオブジェクトは stored が参照するため存続

  • Clear を呼ぶと stored が null になり、他に参照がなければ GC 対象になる


✅ まとめ

項目

C/C++

C#(参照型)

ローカル変数のメモリ

スタック

参照はスタック、実体はヒープ

スコープ終了でメモリ破棄

される

されない(参照があれば存続)

他から参照されていれば存続

✖️

GCによるメモリ回収

✖️(手動)

✅(自動、ただし非即時)


ご参考までに:struct 型(値型)は例外

C# でも struct(値型)は スタックに配置され、スコープを抜けると即座に破棄されますC++ に似た動作)。


(私)

追加で質問します。↑の例、 class Container { MyClass stored; void Store() { MyClass temp = new MyClass(); stored = temp; // フィールドが参照 } void Clear() { stored = null; // この時点で参照が切れ、他に参照がなければGC対象に } } MyClassstoredが、実体(= new (コンストラクター);)も参照もない場合、そのメソッドを使ったり、アクセスするコードを書くとコンパイルエラーになります。storedtempを参照した後、tempが廃棄(= null;)され、従ってstoredも動的に実体を失うことが有る場合、その後にstoredにアクセスすると、実行中のエラーが発生するという認識でよろしいでしょうか?

ChatGPT:

はい、ほとんど正しい理解です。ただ、いくつか重要な点があるので、わかりやすく整理しながらお答えします。


🔹 質問の要点まとめ

  • stored は MyClass 型の参照であり、temp から参照を受けている。

  • temp = null; とすれば stored も連動して参照を失うのか?

  • stored が参照も実体も持たない場合、アクセスするとどうなるのか?


✅ 回答

1temp = null; は stored には影響しません

csharp
コピーする編集する
MyClass temp = new MyClass();
stored = temp;
temp = null; // ← これは temp だけが null になる

この時点で:

  • stored は依然としてオブジェクトを参照しており、実体は存続しています

  • C# は 参照(ポインタ)をコピーしているので、temp の参照が切れても stored の参照は独立して残ります

したがって:

temp を null にしても、stored は still valid です

2stored に何も代入していなければ、null なのでアクセス時に**実行時例外(NullReferenceException**が発生する

csharp
コピーする編集する
class Container {
    MyClass stored;

    void Use() {
        stored.SomeMethod(); // ← stored null の場合、ここで例外
    }
}
  • C# のクラスのフィールド(stored)は、初期化されていなければ自動的に null

  • そのため、明示的に new しておかないと、アクセス時に NullReferenceException が発生します


3C# では「参照が null でもコンパイルは通る」

つまり:

csharp
コピーする編集する
stored.SomeMethod(); // ← stored null でも「コンパイルは通る」、ただし「実行時エラー」

これは C/C++ と異なる点です:

  • C++:未初期化ポインタにアクセス → 未定義動作(クラッシュ含む)

  • C#null 参照 → NullReferenceException が投げられる


✅ まとめ

状況

結果

stored = temp; temp = null;

stored は生きている(影響なし)

stored が何も代入されていない(null のまま)

stored.SomeMethod() などは 実行時エラー(NullReferenceException

stored = null; の後にアクセス

同様に NullReferenceException


🔹 防止策:null チェック

C# では null による実行時エラーを防ぐために、次のように書くのが一般的です:

csharp
コピーする編集する
if (stored != null) {
    stored.SomeMethod();
}

または、C# 6以降なら null 条件演算子 ?. を使う方法もありますが、C# 5 の場合は手動でチェックする必要があります。


✅ 結論

  • temp = null にしても stored の参照は生きている(コピーだから)
  • stored が null のまま、あるいは stored = null の後にアクセスすると 実行時エラー(NullReferenceException
  • コンパイルは通るが、実行時の安全性には注意が必要

 

禅問答は、まだ続きますが、紙面(6,000字)を超えたので、次の回に回します。

 

シリーズ

 

私は元々1985年頃からC言語(8bitのBDS-C)を使うようになり、その後米国駐在時代にC++(Borland Turbo C++ for DOS)を知るようになりました。何れの言語もアセンブラーに近く、ハードウェアの動きを知らないと正しくプログラミングできないような低級言語でしたので、「プログラミングミス == コンピューターの暴走」というのが当時の常識でした。

 

所がOS(Windows)の高度化、堅牢化により、「コンピューターの暴走」は先ずなくなり、エラー表示(+実行スレッドの終了)だけで済んでしまいますし、MicrosoftのCLI(Common Language Infrastructure)のおかげで、C的言語でもプログラマー自身がメモリー管理を行わず、CLIのランタイム(CLR)が自動的にGC(Garbage Collection)を行ってくれます。

 

しかし、

 

その結果プログラムされたコードをコンピューターがどのように処理(特にメモリー管理)しているかがプログラマーには直観(直感ではありません)できず、自分で確認することが必要となります。

 

例えば、

 

C#で、単純に

 

【C#】

int i = 10; または (宣言後)i = 10;

string str = "Hello, world!"; または (宣言後)str = "Hello, world!";

 

という整数型や文字列型オブジェクト(インスタンス)に「変数宣言、初期設定または代入」を行う式を見ていると、C++の同様の式、

 

【C++】

int i = 10; または (宣言後)i = 10;

char* str = "Hello, world!"; または (宣言後)str = "Hello, world!";

char str[14] = "Hello, world!"; または (宣言後)strcopy(str, "Hello, world!";    //文字列13文字 + ヌル終端

 

を思い浮かべてしまいますが、実は全く違います。C++では宣言時にメモリーにint型変数の値の長さ分のメモリーを(newmalloc等でヒープ領域に確保しないのであれば)スタック領域に確保し、そこに(システムによって異なるが32ビットの)'000AH'(十進法の10)を、char*は他に確保した文字列(char[文字数+1]配列)のメモリーアドレスを参照するポインター変数(長さはアドレス長)でしかないのですが、C#では"int"はInt32という構造体の、stringStringクラスの別名(alias)です。()従って"="という等号記号を用いていますが、実は初期化や値取得のメソッド等を実行しています。次は(初期化をどうやっているのかはよくわかりませんでしたが)大体↑のC#のプログラムと同じようなものです。

構造体とクラスの重要な相違は↓で触れます。

 

Int32 i = new Int32();
i = 10;
//ご参考:これとかこれ

//String str = new String("Hello, world!");    //「string' から 'char*' に変換できません」エラー
String str = new String("Hello, world!".ToCharArray());    //.ToCharArrayはStringクラスのメソッド
str = "Hello, world!";
//ご参考:これ
 

なお、以降は構造体は除けておき、クラスに話を絞ります。(

:構造体とクラスの対比を以下に載せておきます。

 

   構造体   クラス
オブジェクトの型  値型   参照型
クラスの継承  不可   可能
フィールド初期化子の使用  不可   可能
デフォルトコンストラクタ(引数なし)の定義  不可   可能
デストラクタの定義  不可   可能

 

で、私が興味を持ったのは

 

C#はC++と似たようなコードでも、実際にはメモリーを直接アクセスするようなことはせず、

 

【値保有-newでメモリー領域を確保する】

クラス名 インスタンス名 = new クラスコンストラクター; //コンストラクターに引数可、オーバーロード可

 

【値参照-データメモリー領域は確保しない】

クラス名 インスタンス名 = (他の)クラスインスタンス; //他のクラスインスタンスを参照するだけ

 

という点です。C++にも似たようなところがありますが、

 

【値保有-newでメモリー領域を確保する】

クラス名 インスタンス名 = new クラスコンストラクター; //コンストラクターに引数可、オーバーロード可

(C++ではnewしてヒープメモリーを確保したら、必ずdeleteでメモリーを開放しないとメモリーリークになる。)

 

【値参照-データのメモリー領域は確保しない】

クラス名* インスタンス名 = (他の)クラスインスタンス; //他のクラスインスタンスを参照するだけ

(C++では参照はポインター(型名+'*')を使い、他でメモリー領域を確保したインスタンスのアドレスが入る。)

 

C++で参照はポインター(*)であることがコード上、一目瞭然ですが、C#では一見違いが分からない」ことです。

このことから、参照型のインスタンスの参照先をGCに出してしまうと「オブジェクト参照がオブジェクトインスタンスに設定されていません。」エラーとなるので注意が必要です。又、値保有型のインスタンスに'='等号記号で代入を行っても、必ずしも代入元のメンバーの値が代入先にコピーされるわけではないので注意しないとなりません。

 

C++とC#の「参照」の主な違い(ざっくり比較)

観点 C++ C#
オブジェクトの標準的な扱い
 
値型(スタックに置かれる)
 
参照型(ヒープに置かれる)
 
= の意味(クラスの場合)
 
コピー(明示的にコピーコンストラクタ)
 
参照のコピー(ポインタのようなもの)
 
メモリ管理
 
手動(newdelete
 
自動(GC)
 
構造体(struct
 
通常でもメンバー関数を持てる(やや柔軟)
 
値型、基本的に軽量でコピー主体
 
デフォルトのコピー動作
 
シャローコピー
(必要に応じて明示的に定義)
シャローコピー
classでは参照、structではコピー)

 

 

と、まぁ、こんなことを偉そうに書いているのは、

 

 

現在CのプログラムをC#に移植しようとして、ポインター引数をrefで書いたり、メソッド内でデータを保有しているのか、参照しているのか、に注意を払っている」ので、皆さんと注意を共有したいと考えただけです。

 

悪しからず。

 

ps. 上を書いたのは結構前なので、C#のクラスと構造体の違いを補足すると「(名前の付いた)変数が参照(変数のデータはアドレス)するのは、クラスのインスタンスではヒープ領域のメモリー、構造体はスタックに積まれたメモリーだそうです。(従ってCやC++と同じく、プログラムブロックを抜けると消滅する揮発性があります。)

 

オリジナルのCプログラムの、「(危害メモリーリーク)を取り除いた)現実的な移植ベースプログラム」の姿形が前回で出来たと思います。

 

これをウィンドウベースのドンガラに適合させるために、C#プログラムに移植する訳ですが、その際に注意すべきこととして、

 

(1)「貴方と私がどういう人なのか(移植元と移植先のプログラミング言語の仕様と特徴)」を事前に理解して、

(2)「貴方と私の(他方にないものや同様に見えて動作の異なるもの、要すれば)相違」を、

(3)「どのように合わせてゆくか(移し替えるか)」を予め考えておく

 

ことが非常に重要です。(要すれば、それが世の定め、ということですか?)

 

ここで私がCで書かれた前回のベースプログラムをC#に移植したコードを紹介するよりも先に、移植にあたり(また移植中に)私が検討した(または遭遇した)C(及びC++)の仕様と特徴とC#との相違...

 

ん?

 

と、ここにきて私がそんなにMicrosft C#()の仕様についてそんなに詳しくない(こうだろうとは思っていても、裏を採ってはいない)ことが自覚されました。

:最新版はC#13ですが、私はVisual Studioを使っていないので、WIndows 11標準装備のC#5でプログラミングしています。詳しくはこちら

 

ということで、

 

C(やC++)とC#との相違、特にこのプログラムに関しては

 

「値渡しと参照渡し」と「メモリー管理」

 

についてもう一度しっかり復習することにし、この手のレッスンには優秀なChat-GPT様にご指導をお願いしようと思い立ちまして、C#の移植版(8Puzzle.cs)を紹介する前に、3回ほど

 

【C#】どうでもよいけど、知っておくとよい話

 

を挿入したいと思います。それを踏まえて、どのようにベースプログラムをC#プログラムにしたのかを、その後に説明させていただきます。

 

【8Puzzle】シリーズにも書きました)が、今まではドツボに嵌った際にいつも手を差し伸べてくれたChat-GPT様が今回は余り頼りにならない、というよりも「全く助けにならないどころか、あれは、これはと色々と言ってくるけど、どれも的を外しており、手間が増えるだけで、5打席ノーヒット」という感じです。

(引用)「途中からChat-GPT様にも相談し、しかし時々注意書きの「ChatGPT の回答は必ずしも正しいとは限りません。重要な情報は確認するようにしてください。」という事態も発生

 

未だに「悪戦苦闘中」ですが、オリジナルのCプログラムのアルゴリズムを生かして、「C#らしい(C#の特性を生かした)」別プログラムを書いて、Chat-GPT様に相談した所、散々振り回された挙句、例えば、

私の前の提案が不適切だった点

  1. correct[]expected[] の使用を提案したのは、仕様が未明で、空白に特殊な扱いがある場合を想定してしまったためです。

  2. Goal = {1,2,...,8, Blank} のような配列を提案しましたが、

    • これは 値8が重複するリスクがあり破綻しています

    • そもそも ID 8 が普通にピースIDとして使われているという前提を見誤っていました。

  3. iPieces[i]の比較が「不合理では」と述べましたが、この実装においては100%合理的かつ最も明快な判定方法です。

🎯 結論:現在の完成判定コードで完全に正しい

for (int i = 0; i < Num; i++) if (Pieces[i] != i) return false; return true; 

これはこのプログラム設計において、最も簡潔かつ正確な完成判定方法です。
また、Bitmap添字との整合性、ピースの識別、完成形の定義にもすべて適合しています。

 

という結末。

 

過ちを認める潔さには敬服

 

するものの、正直、ドット疲れが出て、

 

「人間は疲れるんです...」

 

と大人げない嫌味を言ったら、

 

🔋 まずは休息を

人間にはこのデバッグ作業は疲れます。先ずは疲労回復を優先させます。

まったくその通りです。
論理的な調査は体力・集中力の消耗が大きく、特に**「動いているが正解に届かない」**タイプの不具合は心理的にも消耗します。

温かい飲み物や短時間の仮眠、目を閉じるなどが効果的です。
必要があれば、状況やログの記録をメモだけ残して、今日はシャットダウンするのも選択肢です。

 

有難いんですけど、一寸

 

イラッ💢

 

としてしまいました。

 

今回は彼を当てにせず、

 

自分でシッカリと解法を考えよう!

 

前回掲載したオリジナルのCプログラムいかがでしたでしょうか?「よく分からなかった」というのが普通で、且つあれには重大な欠陥があるのです。それを今回解説してゆきます。

 

先ず「このプログラムは何を行っているのか」をザックリと解説します。

 

(1)これは8パズルを解くプログラム(所謂”Solver”です。

 

(2)最初に構造体BOARDがあり、「あー、8パズルの盤面データ(cell)だな」、と気がつかれるでしょう。またBOARD盤面へのポインターがnextbackのふたつがあり、所謂「二分木」の構造を持っています。

が、

これが曲者で、

盤面データとして使う場合は、「縦の関係」でnextが「子」でbackが「親」なのですが、

盤面データの集合体Queのつもりでq0q1q2にしていると思いますが、実際にはFILOで使っていますね)の場合は、「横の関係」でnextが「次(next)」でbackが「前(previous)」の意味になります。

更に、プログラムではboBoard Original?)という初期盤面と、bgBoard Goal?)という「完成形(データがセル<cell>なのでドラゴンボールっぽいですね!)盤面のデータを持っています。→「完成形{1, 2, 3, 4, 5, 6, 7, 8, 0}(後に重要なカギとなる。)

 

(3)これらを使って行う処理としては、最初の「初期盤面」の空白ピース("0”)を「上下左右(実際の順は上左右下 )」に(動かせれば)動かし(expand関数)、それを新しい盤面データとして(ヒープ)メモリー上に作成(malloc関数し、これが「完成形」であるか否かをチェックし、そうであればプログラムは完了し、そうでなければq2の連鎖(リンク)に追加します。exchange関数)

 

(4)q2の連鎖(リンク)に追加する関数(putq関数)では、先ず過去の盤面データ(q0)に同じものがないかチェック(equal関数)し、有れば作成した盤面データを廃棄(free()関数し、無ければ新規作成中のq2連鎖の最後(新規の場合q2自体、そうでなければ連鎖の最後のnextポインター→共に値はnullに追加します。

 

(5)この「上左右下チェック→新規作成・完了確認→重複チェック・q2登録」迄をmain関数の"while(q1 != NULL)”ループで行い、親→子→孫...というように新規世代(これを「幅優先検索」と言います)を作ったら、現在の親世代の盤面(q1)は「過去の盤面」としてq0に代入し、新規に作成されたq2を次の親としてq1に代入し、q1の「親盤面」の次世代の盤面を(一旦NULLに初期化した)q2に同じように探索させてゆきます。これを図で表すと次の様になります。

 

(解説:これ自分で作りました。結構時間かけてますよねぇ。尚、✖が付いているのは重複盤面です。)

 

一番上の親から探索が上から下へ、左から右へZ状のジグザグに進んでゆくのが分かるでしょう。

 

(6)最後にもう追加する盤面がなくなると、q2は初期化されたままのNULLでループを抜けます。するとq1もNULLが代入されwhileループも終了することになります。この処理数は理屈としては「0-8までの9の数の階乗の半分(9! / 2 = 181,440」にもなります。(ご参考:「8パズルが完成するか否かは、08までの配列を完成させるまでの置換のパリティ(偶奇)と「空き」の最短のマンハッタン距離の偶奇が等しい 」)」

 

(7)終了時に「完成形」となった盤面から、親へ、親へと辿り、「初期盤面」迄最短のルート)を表示します。

:↑の図のようにキュー(q0、q1、q2)として使う時は左から右への横方向ですが、盤面データは前の世代、次の世代(縦方向)になっていることに注意しましょう。

 

しっかし、

 

冒頭でお知らせしたように、本プログラムには重大な欠陥があります。、それは

 

プログラム実行中に10万以上も盤面データをヒープ領域にmalloc関数で確保しておりながら、プログラム終了時でもこれらを開放していない(所謂「立つ鳥跡は泥だらけ」状態)

 

事です。この為、私のプログラムでは

 

(1)コメントを付けて処理の内容や流れを分かり易くした。

(2)メモリー管理に関して自作のスタックとpush関数、release_stack関数を設け、「立つ鳥跡を濁さず」にしました。

 

このプログラムで「解けるサンプルパズル」と「解けないサンプルパズル」を実行した画面を示します。

 

「解けるサンプルパズル」

 

「解けないサンプルパズル」

(理論上の9!以上に盤面を登録しているのは何故か、もう考えるのも嫌です...)

 

そしてこれからこのプログラムをC#のものに移植して行こう、というわけです。

 

8Puzzle.cpp】(.cppですが、表記はCです。)

#include <stdio.h>

#include <stdlib.h>

#include <conio.h> //getch()使用の為

 

#define TRUE 1

#define FALSE 0

 

struct BOARD {

char cell[9];

struct BOARD *next; /* Link to the scenario on the same level */

struct BOARD *back; /* Link to the parent scenario */

};

 

//可解パターン /* Initial state */

//struct BOARD BO = {2, 3, 5, 7, 1, 6, 4, 8, 0, NULL, NULL};

//不可解パターン /* Initial state */

struct BOARD BO = {1, 2, 3, 4, 5, 6, 8, 7, 0, NULL, NULL};

//完成形 /* State at goal */

struct BOARD BG = {1, 2, 3, 4, 5, 6, 7, 8, 0, NULL, NULL};

//q0は過去の登録盤面キュー、q1は現在の親 キュー、q2は現在の「子探索」処理キュー

struct BOARD *q0 = NULL, *q1 = &BO, *q2 = NULL;

 

//----------------------- 追加部分 ---------------------------------

/* exchangeで生成されたboard m用のメモリーを解放する

為、登録可能な最大数(9! - 1)のスタックを設け、

登録(push)出来るようにする。

*/

#define STACK 725760 //理論値は9!362,880)で最大実測値は485,190

struct BOARD **stack; //スタック領域のメモリーハンドル(追加)

unsigned int stack_counter; //スタックカウンター

 

void init_stack() { //BOARD *mのスタック設定(追加)

 

    stack = (struct BOARD**) malloc(STACK * sizeof(struct BOARD*));

    if(stack == NULL) {

        printf("スタック領域を確保できませんでした。\r\n");

        exit(2);

    }

    stack_counter = 0;

}

 

void push(struct BOARD *adr) { //BOARD *mのスタック追加(追加)

 

    stack[stack_counter] = adr;

    stack_counter++;

    if(stack_counter == STACK) {

        printf("スタック領域を超過しました。\r\n");

        exit(2);

    }

}

 

void release_stack() { //BOARD *mのスタック解放(追加)

 

    while(TRUE) {

        --stack_counter;

        free(stack[stack_counter]);

        if(!stack_counter) //if(stack_counter == 0)

            break;

    }

    free(stack);

    printf("スタック登録BOARDデータとスタック領域を解放しました。\r\n");

}

//-------------------------------------------------------------------

 

void printboard(struct BOARD* b) {

 

    //BOARD b3 x 3の盤で表示する

    int i;

    for(i = 0; i < 9; i++) {

        printf("%1d", b->cell[i]);

        if(i % 3 == 2)

            putchar('\n');

    }

    putchar('\n');

}

 

void traceback(struct BOARD* b) {

 

    //最後のBOARDの親を辿って最後にBO(最初のBOARD)迄表示する

    int i = 0;

    for(; b != NULL; b = b->back) { //iは交換数(追加)

        printboard(b);

        i++; //交換数(追加)

    }

    //パフォーマンスデータ表示(追加)

    printf("%s%d%s%d%s", "traceback終了(盤面交換数:", i, "、登録盤面数:", stack_counter, "\r\n");

}

 

bool equal(struct BOARD* b1, struct BOARD* b2) {

 

    //b1b2が完全に同一か確認する

    int i;

    for(i = 0; i < 9; i++) {

        if(b1->cell[i] != b2->cell[i])

            return FALSE; /* False */

    }

    return TRUE; /* True */

}

 

void putq(struct BOARD* b) {

 

    //過去の盤面キュー(q0)のnext連鎖を辿り、bと同じものがあれば戻る

    struct BOARD* n;

    for(n = q0; n != NULL; n = n->next)

        if(equal(b, n)) {

            free(b);

        return;

    }

    //q2に登録が無ければ新しいBOARDを登録する

    if(q2 == NULL)

        q2 = b;

    //新しいBOARDで、q2に登録が有れば、q2の最後まで辿り

    else {

        for(n = q2; n != NULL; n = n->next) {

            //同じBOARDがあれば戻る

            if(equal(b, n)) {

                free(b);

                return;

            }

            if(n->next == NULL)

            break;

        }

        //新しいBOARDq2の最後(next)に登録する

        n->next = b;

    }

}

 

void exchange(struct BOARD* b, int i, int j) { //引数iは値0の位置

 

    //BOADインスタンスmを生成する

    struct BOARD *m;

    m = (struct BOARD*) malloc(sizeof(struct BOARD));

    if(m == NULL) {

        printf("Memory overflow!\n");

        exit(2);

    }

    push(m); //生成したboard盤面データアドレスを保存する(追加)

    //引数bのボードをmにコピーする

    int k;

    for(k = 0; k < 9; k++)

        m->cell[k] = b->cell[k];

    //bjの値をmiに、mjには値0bi)を代入する(mbijを交換したBOARD

    m->cell[i] = b->cell[j];

    m->cell[j] = 0;

    //mback(親)にはbを、nextにはNULL(新しいBOARD)を代入する

    m->back = b;

    m->next = NULL;

    //完了確認(mBG<完了形>であれば、そう表示して終了

    if(equal(m, &BG)) {

        traceback(m);

        printf("equal(m, &BG)が真\r\n");

        //盤面(BOARD)アドレス用スタックのデータとスタック領域の解放 (追加)

        release_stack();

        getch();

        exit(0);

    }

    //完了しなければキューにmを置く

    putq(m);

}

 

void expand(struct BOARD* b) {

 

    //0の配列位置を探す

    int i;

    for(i = 0; i < 9; i++) /* Looking for where "0" is */

    if(b->cell[i] == 0)

        break;

    //3 x 3盤の下2行にある場合

    if(i > 2)

        exchange(b, i, i - 3); /* Above */

    //3 x 3盤の右2列にある場合

    if(i % 3 > 0)

        exchange(b, i, i - 1); /* Left */

    //3 x 3盤の左2列にある場合

    if(i % 3 < 2)

        exchange(b, i, i + 1); /* Right */

    //3 x 3盤の上2行にある場合

    if(i < 6)

        exchange(b, i, i + 3); /* Below */

}

 

int main(int argc, char** argv) {

    //exchangeで生成する盤面(BOARD)アドレス用スタック

    init_stack();

    // BOARDポインターbを、初期状態が入ったq1から始めて、

    // nextBOARDNULL(他に打つ手がない)になるまで

    // expandで可能な手を探し、キュー(q2)に入れる。

    struct BOARD *b;

    while(q1 != NULL) {

        for(b = q1; b != NULL; b = b->next)

            expand(b);

        // q2→q1→q0へシフトを繰り返し、q2NULL(全ての可

        // 能な手が尽くされた)となった際に、q0にその結果が

        // 入り、q1NULLとなり、ループを抜ける。

        // (但し実際の終了はexchangeとなる。)

        q0 = q1;

        q1 = q2;

        q2 = NULL;

    }

    //失敗した場合の盤の推移を表示(追加)

    for(b = q0; b->next != NULL; b = b->next);

    traceback(b);

    //失敗の表示(追加)

    printf("全ての盤面検索を終了しましたが、完成形はありませんでした。\r\n");

    //盤面(BOARD)アドレス用スタックのデータとスタック領域の解放(追加)

    release_stack();

    getch();

}

 

----------------------------------

【注意】以下の文章は昨日以前に書かれましたが、本朝(2025年6月15日)悪戦苦闘は解消されましたので、書かれている内容の一部(赤下線)に事実誤認があります。このシリーズの最後に「真犯人」が明らかにされますが、それを踏まえてお読みください。

----------------------------------

 

前回、C#のウィンドウプログラムに導入するベースプログラムとして、昔入力したC言語のプログラム()を選定しました。

 

:「今を去る半世紀弱の昔、私がC言語(C++ではないですよ!)を学び始めた1986年に購入して勉強した「実戦Cプログラミング」(略)にあったC言語による8パズルの解法プログラム...ソースコードがあるといっても、当時のプログラムは表記が現在とはやや異なり、メモリー対策からコメントも限定されているために、正直内容を理解せずに本からインプットしただけものであることから、改めてその内容を理解することから始めました。

 

前回も「一見親類かと思うけど、実は素性は結構違う」とかきましたが、似て非なるとはまさにこのこと、と言えるCとC#の違い(基本的にそのメモリー管理と参照渡し、値渡しの問題なんですが...)から、とうとう「悪戦苦闘の上、一旦すべて白紙に戻し、C#用に再設計する所からやり直す」ことを決定しました。

 

そんな訳で、

 

当該Cプログラムは、「考え方」と「処理フロー」を参考にする形にとどめるわけですが、今までの試行錯誤の歴史が少しは皆さんに役に立つかと、または単にブログテーマとして腹を抱えて笑っていただけるかと思い、私の悪戦苦闘の有り様も書いてゆこうかと思います。先ず今回はオリジナルのコードの紹介から。(これを一目見て何をしているのか、が分かる方はすごいと思いますよ。又本当のオリジナルは、

 

void foo(a, b, c)

int a; char* b; double c; {

...

}

 

の様な記法を採っていましたが、これは現代風に改めています。又当時の変数をどのようにメモリーやCPUに置くか、という指定のautoとかregisterを使いましたが、これも改めています。)

 

【8Puzzle.c】

#include    <stdio.h>

struct BOARD {
    char cell[9];
    struct BOARD *next;    /* Link to the scenario on the same level */
    struct BOARD *back;    /* Link to the parent scenario */
};

/* Initial state */
struct BOARD BO = {
    2, 3, 5, 7 ,1, 6, 4, 8, 0, NULL, NULL
};
/* State at goal */
struct BOARD BG = {
    1, 2, 3, 4 ,5, 6, 7, 8, 0, NULL, NULL
};

struct BOARD *q0 = NULL, *q1 = &BO, *q2 = NULL;

void printboard(struct BOARD* b) {

    int i;
    for(i = 0; i < 9; i++) {
        printf("%1d", b->cell[i]);
        if(i % 3 == 2)
            putchar('\n');
    }
    putchar('\n');
}

void traceback(struct BOARD* b) {

    int i = 0;
    for(; b != NULL; b = b->back) printboard(b);
}

bool equal(struct BOARD* b1, struct BOARD* b2) {

    int i;
    for(i = 0; i < 9; i++) {
        if(b1->cell[i] != b2->cell[i]) return 0;
    }
    return 1;
}

void putq(struct BOARD* b) {

    struct BOARD* n;
    for(n = q0; n != NULL; n = n->next)
        if(equal(b, n)) {free(b); return;}
    if(q2 == NULL) q2 = b;
    else {
        for(n = q2; n->next != NULL; n = n->next) {
            if(equal(b, n)) {free(b); return;}
        n->next = b;
    }
}

void exchange(struct BOARD* b, int i, int j) {

    struct BOARD *m;
    m = (struct BOARD*) malloc(sizeof(struct BOARD));
    if(m == NULL) {
        printf("Memory overflow!\n");
        exit(2);
    }
    int k;
    for(k = 0; k < 9; k++) m->cell[k] = b->cell[k];
    m->cell[i] = b->cell[j];
    m->cell[j] = 0;
    m->back = b;
    m->next = NULL;
    if(equal(m, &BG)) {traceback(m); exit(0);}
    putq(m);
}

void expand(struct BOARD* b) {

    int i;
    for(i = 0; i < 9; i++)    /* Looking for where "0" is */
        if(b->cell[i] == 0) break;
    if(i > 2) exchange(b, i, i - 3);        /* Above */
    if(i % 3 > 0) exchange(b, i, i - 1);    /* Left  */
    if(i % 3 < 2) exchange(b, i, i + 1);    /* Right */
    if(i < 6) exchange(b, i, i + 3);        /* Below */
}

int main(int argc, char** argv) {
    struct BOARD *b;
    while(q1 != NULL) {
        for(b = q1; b != NULL; b = b->next) expand(b);
        q0 = q1; q1 = q2; q2 = NULL;
    }
}
 

ここ迄読んで、「なんのこっちゃ?」と思われた方、今後もっとコメントを増やして、日本語で何をやっているのかを私の修正プログラムと共に解説しますから、今回はかる~~~~く眺めるつもりで結構ですよ。

 

前にこんなことを書きましたが、今回の8Puzzle問題でChat-GPT様と会話をしている中でこの話も出て、次のようなアドバイスをもらいました。

(Chat-GPT)そして『シャッフル方式の方が速いと思っていたのに、実は乱数生成方式の方が速かった』という点は、多くの人が直感と違うと感じるところでもあります。でも、実際には**その逆(乱数方式の方が遅い)**になるのが通常で、もし 乱数生成方式の方が速い という結果になったとすれば、以下のような要因が関係している可能性があります。

〇ShuffleArr() は「Fisher–Yates シャッフル(Knuth shuffle)」の一種であり、O(n) 時間で完了します。アルゴリズム的には非常に効率的です。

 

〇Array.IndexOf(arr, val) が 毎回 O(n) かかり、val がすでに入っていれば やり直しが発生する為、終盤ではほとんどの値がすでに存在するため、ループ回数が激増します。

では、なぜ GenRandArr() の方が速かったのか?これはおそらく
計測ミスです。Stopwatch の Start() が2回使われているのに、Reset() や Restart() がないため、2回目の計測で 前回の時間が累積されている 可能性が高いです。

 

実際その指摘に従って、二回目の計測にRestart()メソッドをつかったら以下の結果になりました。

 

【乱数生成方式】
処理時間:2ミリ秒

【シャッフル方式】
処理時間:0ミリ秒

 

ウ~ン、流石Chat-GPT様、

 

助かります。

 

ps. 後のブログで書きますが、今回の悪戦苦闘の原因は「人間の私」にありました。それまでは「あのChat-GPTのバカ野郎、態度がタカピー、上から目線のくせに間違いばっかり教えやがって。チッ!」と思っていたので、恥じ入るばかりです。もっと人間を磨き、徳を積まなければ。