値型と参照型

データ項目の型によって、そのデータを保存するのに必要なメモリサイズ(ビット数)と、型に内包されるデータメンバーが決まります。また型はオブジェクトのメモリ上の配置先、スタック(Stack)かヒープ(Heap)かを定義します。

型は大きく二種類に分かれます:値型(Value Types)参照型(Reference Types)。この二つのオブジェクトはメモリ上の保存方式が完全に異なります。

1、値型は一つのメモリ領域だけを使用し、この領域に生のデータが直接格納されます。

値型のデータはスタックStackに直接配置され、Data領域に実データが保存されます。例えばint age = 12; の場合、12という数値がそのままスタックに記録されます

2、参照型は二つのメモリ領域を必要とします:

  • 一つ目の領域に実データが保管され、こちらは常にヒープに確保されます;
  • 二つ目は参照アドレスで、ヒープ上のデータの位置を指します。(スタックにはヒープのアドレスが保存され、C言語時代にはポインタと呼ばれていました)
参照型の本体データはヒープに配置され、000001のようなアドレスが割り当てられ、スタックにはこのアドレスの参照情報が格納されます

上記二つの図は、オブジェクトが他のオブジェクトのメンバーではない単独の場合に限って適用されます。

注意点

参照型オブジェクトの本体データ部分は、図の通り必ずヒープに配置されます。

一方、値型のインスタンスや参照型の参照アドレスは、コードの記述によってスタックとヒープどちらにも配置される可能性があります。

例を挙げます:

Personという参照型クラスがあり、値型メンバーと参照型メンバーが一つずつ定義されているとします。

多くの開発者は「値型メンバーはスタック、参照アドレスもスタック、本体データはヒープ」と誤解しますが、この認識は間違いです。

覚えておくルール:参照型インスタンスの全ての本体データは永久にヒープに配置されます。二つのメンバーはいずれもこのオブジェクトの本体データに属するため、値型・参照型を問わずまとめてヒープ内に保存されます。

サンプルコード:

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        Person bill = new Person("Bill", 18);
    }

}

class Person
{
    private string name;

    private int age;

    private Phone myPhone;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
        this.myPhone = new Phone();
        this.myPhone.number = 12345;
    }
}   

class Phone
{
    public long number;
}Code language: JavaScript (javascript)

上記コードを見ると、Personクラスには基本型のage、nameのほか、独自定義型のPhoneが含まれます。Phone型にはさらにlongの基本型が定義され、電話番号を保管しています。

読者はPerson内のint ageやPhone内のlong numberを見て、値型なのでスタックに保存されると思うかもしれませんが、実際は違います。これらは基本組み込み型ですが、独自クラスのメンバー変数として定義されているため、インスタンスと共にヒープに配置されます。実際のメモリ配置は以下の通りです。

図から分かる通りint ageとlong numberは組み込みの値型ですが、独自型のメンバーなのでヒープに保存されます。myPhoneには参照アドレスが格納されており、参照型の参照情報もヒープに配置されるケースがあることが確認できます。

核心ルール:独自定義クラスのメンバー変数は値型・参照型を問わず全てヒープに配置されます。例のmyPhoneのようにクラス内に参照型メンバーが存在する場合、そのフィールドには別のオブジェクトのヒープアドレスが記録されます。

もしPhoneクラスに別の独自型が追加された場合、Phoneのメモリ領域に新たにそのオブジェクトへの参照アドレスが保存されます。

プログラムはMain関数から実行が始まり、最上位の参照オブジェクトのアドレスがスタックに格納されます。その後、連鎖的に複数のオブジェクトがヒープに繋がっていく構造です。

参照は鎖のように連なる

別の例

// 参照型クラス
class MyType
{
    public int A;       // 値型メンバー、ヒープ上のMyTypeインスタンス内部に内包
    public Person B;    // 参照型メンバー、参照ポインタはヒープに、本体データは別のヒープ領域に確保
}

class Person
{
    public string Name;
}

void Test()
{
    // スタックにmyObjの参照ポインタが保存
    MyType myObj = new MyType(); 
    /*
    ヒープレイアウト:
    MyTypeオブジェクト {
        A: インラインに保存されたint値
        B: Personを指す参照ポインタ
    }
    別のヒープ領域:Person{ Name: ... }
    */

    // ローカル値型変数、スタックに直接配置
    int localNum = 10;
}Code language: PHP (php)

上記ルールはクラスのメンバー変数に限り、クラス内のメソッドには適用されません。メソッド内でローカル変数や参照オブジェクトを宣言した場合、ローカル値型と参照オブジェクトのアドレスはスタックに配置されます。下記コードを参照してください。

続いてこちらのサンプルを確認します:

using System.ComponentModel.DataAnnotations;

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        Person bill = new Person("Bill", 18);
        bill.Calculate();
    }

}

class Person
{
    private string name;

    private int age;

    private Phone myPhone;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
        this.myPhone = new Phone();
        this.myPhone.number = 12345;
    }

    public int Calculate()
    {
        int one = 12;
        int two = 23;
        Phone myNewPhone = new Phone();
        return one + two;
    }
}   

class Phone
{
    public long number;
}Code language: PHP (php)

このコードではPersonクラスにCalculateメソッドを追加し、メソッド内に値型・参照型双方のローカル変数を定義し、Mainから呼び出しています。これらの変数はPersonのメンバーではなくメソッド専用のローカル変数のため、値型ならスタック、参照型の参照アドレスもスタックに配置されます。

bill.Calculate(); を実行した際のメモリ状況は下図の通りです

図内の青い領域が、実行中のメソッド内部変数のメモリ配置箇所となります。

メソッドの実行が完了すると、これらのローカル変数はスタックから削除され、メモリ構成は下図に変化します

Calculateの実行が終了し制御がMainに戻ると、メソッド内で作成したローカル変数はスタックからポップされ破棄されます。ただしmyNewPhoneの本体データ(図の青い部分)はヒープ上に残ります。スタックから消えるのは参照アドレスのみで、実データは即時削除されず、ガベージコレクタGCが適切なタイミングで回収するまで保持されます。

値型と参照型

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です