[C#] 11. インスタンスう生成(new)とメモリ割り当て(StackメモリとHeapメモリ)そしてヌル(null)


Study / C#    作成日付 : 2019/07/07 22:54:13   修正日付 : 2021/08/02 15:07:51

こんにちは。明月です。


この投稿はインスタンスう生成(new)とメモリ割り当て(StackメモリとHeapメモリ)そしてヌル(null)に関する説明です。


以前の投稿でクラスを作成する方法に関して説明したことがあります。

link - [C#] 10. クラスを作成する方法(コンストラクタ、デストラクタ)


クラスを作成してインスタンスを生成する方法でnewというキーワードを使います。

using System;

namespace Example
{
  // Exampleクラス
  class Example
  {
    // メンバー変数
    private int data;
    // コンストラクタ
    public Example(int data)
    {
      // メンバー変数設定
      this.data = data;
    }
    // 関数
    public void Print()
    {
      // コンソールに出力
      Console.WriteLine("Data - " + data);
    }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // インスタンス生成
      Example ex = new Example(10);
      ex.Print();
      
      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


ここでExample ex = new Example(int)の意味はExampleタイプのex変数にnew Exampleのインスタンスを生成してintタイプのパラメータを持つコンストラクタを呼び出すことの意味です。

先に確認しべきなところは変数の宣言です。以前に変数宣言に関して説明したことがありますが、変数宣言にはデータタイプと値が一致しなければならないです。つまり、intタイプに実数タイプを格納するか文字列を格納することができないみたいにExampleタイプには必ずExampleクラスのインスタンスが格納しなければならないです。

このExample exは割り当てメモリアドレスを指していることで一応ポインター変数といいます。つまり、変数に値が格納することではなく、メモリアドレスが格納することです。

上のイメージみたいな構造になります。

ここでStackメモリとHeapメモリの構造が表させています。

Stackメモリは我々がプログラムで関数を作成する時に実行領域を設定する中括弧({})があります。この中括弧の領域を我々はStack領域といいます。このStack領域で宣言する変数の値はStackメモリに格納することで思えば良いです。


上のイメージを見ればMain関数の中で任意な中括弧を使って新しいStack領域を生成しました。その新しいStack領域でintタイプのdata変数を宣言しましたが、領域の外側で使ったら存在しない変数というエラーメッセージが表示されます。

つまり、Example exはMain関数のStack領域で宣言した変数という意味になります。


new Example(10)はHeap領域で割り当てしたインスタンスですが、Heapはプログラムの領域のメモリ構造です。つまり、プログラムが実行する時にHeapメモリの領域が生成してそのメモリの領域を自由にインスタンスを生成したり解除したりすることが可能です。プログラムが終了するとHeapメモリは無くなります。

Heapメモリにインスタンスの割り当てはnewキーワードを使って生成が可能し、GC(ガベージコレクション)でメモリ解除が行います。Stack領域の差異はStackアルゴリズムでそのアドレスの位置を探すことができますが、Heapメモリはメモリアドレスを知らないと探すことができません。


改めてまとめるとStackメモリは静的なメモリ領域でデータを探しやすいですが(Stackアルゴリズムのpush popでデータを探す)、Heapは動的なメモリ領域でnewキーワードでインスタンスを割り当てしたらデータポインタだけでデータを探すことができます。

そしてその二つを連携したことがExample ex = new Example(10);の形です。


それならなぜC#にはこんな複雑にStack=Heapの構造でデータを扱うことにするかな?

実のこの概念はC/C++からの概念です。C/C++ではnew(calloc、malloc)を使わなくて、インスタンスをStack領域に割り当てすることができます。原始データタイプ(Primitive type)みたいです。

しかし、このStackメモリが大幅にあることではなく、メモリサイズが決まっています。普通はそのサイズが1MB(Mega byte)から2MB程です。

ポインタ値のメモリサイズはintタイプで4byteですが、役25万個ごろ宣言することができるサイズです。25万個なら凄く大きいと思いますが、実は多くないです。

そして、Stackメモリには変数だけあることではなく、関数の状態(Interrupt)などでデータもあります。そうするとStackメモリのサイズが大きいことではありません。最近のプログラムをみれば何GB(Giga byte)も簡単に使いますが、それを比べたら1MBは凄く小さいことです。そのいうことでStackメモリをすべて使えばStackOverflowが発生します。

それならこのStackメモリ設定を大きく設定すればよいと思いますが、その通りに大きく設定すればStackOverflowは解決できます。でもStackメモリ構造はStackアルゴリズム構造ですが、pushとpopの形でデータを探すアルゴリズムです。Stackアルゴリズムではなく、探索アルゴリズムを持つ構造(データ構造)ならそのサイズが大きくなると遅くなります。

つまり、Stackメモリサイズが大きくなるとプログラムは遅くなります。

なので、C\C++みたいにStackメモリにインスタンスを割り当てすることは無理があります。


HeapメモリはStackメモリみたいに整形化になったデータ構造ではありません。メモリサイズが大きいですが、探索アルゴリズムがないので、データを探すためには必ずメモリ構造が必要です。なのでnew Exampleの表現でHeapメモリにクラスのインスタンスを割り当てして割り当てしたメモリアドレス(ポインタ)をStackメモリに格納します。

そうすると、Stackメモリが1MBで、Heapメモリは何GBを使ってもプログラム性能は速くてメモリを大幅で使える構造になります。


C#でそのアドレス値を確認する方法でGetHashCode関数を使えば確認できます。

using System;

namespace Example
{
  // Exampleクラス
  class Example
  {
    // メンバー変数
    private int data;
    // コンストラクタ
    public Example(int data)
    {
      // メンバー変数設定
      this.data = data;
    }
    // メンバー変数再設定関数
    public void setData(int data)
    {
      this.data = data;
    }
    // 関数
    public void Print()
    {
      // コンソールに出力
      Console.WriteLine("Data - " + data);
    }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // インスタンスを生成
      Example ex1 = new Example(10);
      Example ex2 = ex1;
 
      // ex1変数のメモリアドレスをコンソールに出力
      Console.WriteLine(ex1.GetHashCode());
      // ex2変数のメモリアドレスをコンソールに出力
      Console.WriteLine(ex2.GetHashCode());
      // ex2変数のメンバー変数を再設定
      ex2.setData(20);
      // ex1のメンバー変数値
      Console.WriteLine("ex1 -");
      ex1.Print();
      // ex2のメンバー変数値
      Console.WriteLine("ex2 -");
      ex2.Print();
 
      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


上のソースをみればex1の変数でnewキーワードを使ってインスタンスうを割り当てしました。そしてex2変数にex1変数の値を格納しました。

GetHashCodeの関数を使うと値が同じです。つまり、この意味はex1の変数にあるデータとex2の変数にあるデータは同じインスタンスという意味です。


後でex2の変数にあるインスタンスのメンバー変数のデータを20に変更しました。

結果はex1とex2には同じ結果になりました。

上の形で二つの変数に一つのインスタンスを指していることです。つまり、ex1変数とex2変数は同じインスタンスを指しているからex2の変数にあるインスタンスのデータを変更してもex1の変数にあるインスタンスのデータも影響がなります。


ここまで変数を宣言して、インスタンスを割り当てしてそのポインタを格納することを説明しました。

そうなら変数を宣言する時にインスタンスを割り当てではなく、別にする時にはどうでしょう。


その時には変数に「空いている」の表現でヌル(null)ということがあります。つまり、nullの意味はStackメモリには変数を宣言しましたが、Heapメモリにはまだインスタンスを割り当てしせずに空いているという意味になります。


変数にnullが設定して関数を呼び出すとインスタンスが宣言されてないですという「Null Exception」が発生します。


ここまでインスタンスう生成(new)とメモリ割り当て(StackメモリとHeapメモリ)そしてヌル(null)に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

#C#
最新投稿