C# Tips

値型がスタック領域に置かれない話
(スタックとヒープ)

C#について学んでいると、値型と参照型の違いについての説明で、よく以下のような説明を見かけます。

メモリとかハードウェアの細かい部分をよく理解していない私は、こうした説明を表面的に理解したつもりで

「ふーん、構造体はスタック領域に置かれるのか」と単純に思い込んでいました。


前述のように説明されているページでも、よく読めば必ずしもそうではないことがちゃんと説明されていたりするのですが、自分の場合は理解があまりにも大雑把すぎて、長らく勘違いしたままでいました。


正しくは、値型/構造体はスタック領域に置かれる――メソッドの処理スコープ内で宣言された一時的な変数である限りは」ということのようです。


見る人が見れば「何当たり前のこと言ってんの?」って感じなのかもしれませんが、自分はこの基本的なことがわかっていませんでした。

値型/構造体は、配列の要素であろうがクラスのメンバーであろうが何であろうが、常にスタック領域に置かれると思い込んでいたのです。


(※以下は独学に基づく理解です。実は間違っている可能性とかあります)

スタック領域に作られるもの


ヒープ領域に作られるもの

【メソッドローカル変数】 ここではメソッドのスコープ内処理で一時的に使われる変数を指して使っています。何らかのクラス等のフィールドとして宣言されている変数や配列の要素などは含みません。 単に「ローカル変数」と言ってもいいかもしれませんが、クラスのローカルなメンバー変数などもローカル変数と記述されることがあるっぽいので、敢えて「メソッド」とつけています

ということらしいです。

値型であってもスタック領域に作られるものは一時処理用のものだけで、それ以外は値型であろうと参照型であろうと、大抵はヒープ領域に収まっていることが多いはず。

というだけの話でした。

// ※以下は実験して確かめたものではなく、おそらくこうであろうという推測に基づくものです

//  最適化によるコンパイル後の変更なども考えないものとします


// クラス(参照型)

class MyClass

{

    // ヒープ領域の予約(参照型であるクラスのメンバーなので)

    public int xf;

 

    // ヒープ領域の予約(参照型であるクラスのメンバーなので)

    public MyStruct sf;

}


// 構造体(値型)

struct MyStruct

{

    // MyStructが宣言された領域に一緒に置かれることになるので、

    // ヒープ領域の変数になるかスタック領域の変数になるかはまだ未定

    public int af;

}

 

class App

    void Start()

    {

        int x = 1; // スタック領域に作られたxに1が格納される


        {           

            int y = 2; // スタック領域に作られたyに2が格納され、xの上に積まれる

        } 

   // スコープを抜けたので yはスタックから消される

 

        int[] arrayXs = new int[1]; // ヒープ領域に配列が作られ、参照がスタック領域のarrayXsに格納される)

        arrayXs[0] = x; // スタック領域からxがヒープ領域の配列領域にコピーされる

        int x1 = arrayXs[0]; // ヒープ領域の配列領域から、スタック領域にコピーされる

 

        MyStruct s = new MyStruct(); // スタック領域に構造体が作られる

        s.af = x; // スタック領域のxがスタック領域のs.afにコピーされる

 

        MyClass c1 = new MyClass(); // ヒープ領域にクラスが作られ、参照はスタック領域のcに格納される)

        c1.xf = x; // スタック領域のxがヒープ領域のc.xfにコピーされる

  c1.sf = s; // スタック領域のsがヒープ領域のc.sfにコピーされる


        MyClass c2 = null; // どこも示していない参照がスタック領域のc1に格納される

 

        MyStruct[] array_s = new MyStruct[10]; // ヒープ領域に配列が作られ、参照がスタック領域のarray_sに格納される

        Span<MyStruct> stack_s = stackalloc MyStruct[10]; // スタック領域に配列が作られる

 

        int z = MyFunc(x);       // スタック領域のzに戻り値がコピーされる

    }

 

    // メソッド

    int MyFunc( int value ) // スタック領域に引数のコピー value が作られる

    {

        return value + 1; // 値 value+1 が作られて返される

    }

}

他の人にはどうでもいいであろう経緯

最初におかしいと思ったのは、「Windowsの場合、プロセスに割り当てられるスタックメモリのサイズは通常1MB程度」という説明をネットで見たときでした。

スタックのメモリを使い果たすと、スタックオーバーフロー(Stack Overflow)のエラーになります。実際プログラミング中に何回見ましたが、大抵は凡ミスで無限ループに入ってる場合などに限られていました。


えっ?今まで結構フィールドがいっぱいある構造体の配列とか普通に宣言してたけど、1MB程度の制約の中でそんな危ない橋を渡ってたの?

そんなはずはない。何かおかしいな・・・と思ったわけです。


しかしわかってみれば何のことはなく、値型や構造体でも、配列に入れてしまえば配列自体がヒープ領域に置かれているので、値型や構造体であってもヒープ領域に置かれるというだけの話でした。


そもそも「スタック領域」がその名の通り「スタック方式な領域」であるということを理解していればこうした誤解には至らなかったかもしれませんが、論理思考に乏しかったためか、そういう誤解を抱えたままでした。(スタック:LIFO(Last in First out / 最後に入れたものを最初に出す)というデータのやりくり方式)


勝手に誤解してたというだけの話なのですが、同じ勘違いをしてる人がいるかもしれないので・・・。