Milkのメモ帳

日々の思いつきを忘れないようにのメモ用です。

Milkのメモ帳

【第9回】変数及びクラスのメモリ領域確保(ポインタ)について知ろう【C#】


f:id:maxminkun:20161030140236j:plain Photo via VisualHunt.com

前回は、クラスについての基礎的な考え方を学習しました。

www.milkmemo.com

さて、今まで「変数」と「クラス」という物が出てきましたね。

少しこれを簡単にですが、振り返ってみることにしましょう。


「変数」と「クラス」のおさらい

変数とクラスは共に、モノを入れるという考え方では似たものですが、クラスのほうがもっとくくりが大きいのです。

変数

変数と言うのは、型というものが決まっています。

例えば、int、char、doubleなどと言ったものです。

また、どの型を使うのかを宣言し、それに名前をつけることで利用が可能になります。

//型と変数の宣言。変数を0で初期化。
int suji_mae = 0;

//変数に3を代入。
suji_mae = 3;

イメージとしては、1つの箱を用意して、それに値を入れると思ったほうが良いでしょう。

クラス

クラスとは、以下の内容で構成されるかたまりになります。

  • 変数
  • コンストラクター
  • メソッド

f:id:maxminkun:20161030141140j:plain

なので、クラスとは、変数を内部に幾つも持つことが出来ますし、その変数に対する処理の部分も内包することが出来ます。

また、使い方としては、ちょっと変数とは使い方が異なります。

f:id:maxminkun:20161030141302j:plain

クラスは自分の名前を持っていて、上の例ではCalcクラスという型という扱いになっています。

ソースコードの中でも、「public class Calc」と明記されているはずです。

そして、クラスとは鋳型であるとご説明しました。

実際に利用する場合には、これをコピーしてそっくり同じものを生成(インスタンス生成)して使います。

使い方を見てみましょう。

//Calcクラスを使って、calcAという名前でインスタンス生成
Calc calcA = new Calc();

//Calcクラスを使って、CalcBという名前でインスタンス生成(コンストラクターを利用する場合)
Calc calcB = new Calc(a, b);

また、クラスはインスタンス生成を行う際に、コンストラクターという特別なメソッドが自動的に動作するということもお話しました。

通常は、そのままでもよいのですが、特別に何かしたい場合は、右辺の()の中に必要なデータを渡す等を行います。

ポインタ

さて、実は今回の主なテーマは「ポインタ」になります。

プログラムを勉強したことのある方は、必ずと言ってもいいほど「ポインタに泣かされた・・・」という経験があるはず(笑)

それに、「この嘘つき!C#はポインタを使わないって言ったじゃねーか!!」と怒られるかも(汗)

確かに、C#はポインタを意識して文法的に利用するという機会はありませんし、その部分はOSに任せるという設計がされた言語です。

しかし、ポインタの概念を理解していないと、実際のプログラム動作の時にエラーの切り分けが行えなくなるのです。

また、動作の説明がつかなくなってしまいます。ポインタは表面上は消えてしまいましたが、裏では確実に生きている必須の技術であり、それを頭に入れておく必要があります。

まぁ・・・そう固くならずに、楽な気持ちで行きましょう!

変数やクラスはメモリに確保される

先程まで、「変数」や「クラス」と言っていましたけど、結局の所どこで内容を覚えているのでしょう?

その答えは、メモリです。

皆さんのパソコンにも、メモリ◯◯GB搭載となっているはずです。

今では、4GB以上搭載が一般的かな?

コンピュータは、物事を実行する時に、先ずはメモリに全ての内容を一時的に置きます。そして、それを参照しながら動作を行うのです。

コンピュータのこの部分については以下の記事を参考にしていただければ幸いです。

www.milkmemo.com

さて、メモリ上にあらゆるものが一旦記憶されるのですが、「変数」と「クラス」はメモリ上で記憶される場所が少々異なります。

スタック領域とヒープ領域

メモリの中は様々な区画があるのですが、その中でも皆さんに知っていて欲しいのは、「スタック領域」「ヒープ領域」と呼ばれる区画です。

あぁ・・・別段、ハードウェアの詳しいところまで知る必要はありません。

そういう場所があるんだぐらいで良いです。

簡単に図示するとこういうことになります。

f:id:maxminkun:20161030151351j:plain

変数は、予め型が決まっています。つまり、どれだけの大きさの場所を使うかが分かっているのです。

それに、個々の変数の大きさは小さいです。これらはスタック領域という場所に確保されます。

さて、クラス等の変数以外のものは、基本的にヒープ領域に確保されます。そして、何故分けているかと言うと、このヒープ領域に確保されるものは「可変」のものだと思って下さい。

クラスには様々な変数を内包していましたよね?中には、後から追加したり、削除したりという、リストと呼ばれる可変で増減する機能を持つことも出来ます。こうなると、予めどれだけのメモリ量を使うのか検討がつきません。

そういうものは、ヒープ領域で管理を行い、OSが監視をしながらその使用量の増減を管理するのです。

このままでは、「スタック領域」と「ヒープ領域」の違いは分かっても、どうしてそこまで深刻に考える必要があるのか分かりませんね。

実はプログラムを書く上で、これらは重要な意味を持つのです。

プログラム実装上の違い

これら、どちらの領域に場所を確保するかによって、プログラムの意味が少々変わってきます。

では、これも実際の簡単なプログラムを書いてみましょう。

int A = 10;
int B = 10;
int C = 0;

C = A + B;

さて、変数「C」には何が入っているでしょうか。答えは、20が入っています。

ここまでは良いと思います。

それでは、クラスの場合に同様な処理をしてみましょう。


表示されない場合はこちらを参照ください
第9回プログラミング講座(ヒープ領域)

解説をすると、3行目〜8行目は、Calcクラスの定義を行っています。つまり鋳型を作っています。

13行目と14行目で、newを行うことにより、インスタンス生成を実行しています。(これが、ヒープ領域に場所を確保してねという命令です。)

さて、少々難しくなりますが、17行目と18行目は「.」(これは「の」を意味するんでしたよね。)を使うことで、各々の生成したクラスの内部の変数「suji1」に代入を行っています。

つまり、「CalcAのsuji1に5を代入」「CalcBのsuji1に10を代入」という意味なのです。

では、21行目はどういう意味でしょうか。

CalcBをCalcAに代入します。

このまま行くと、CalcBの内容が全てCalcAに入れられるように思うでしょう?

f:id:maxminkun:20161030161453j:plain

実は、違うことが起きます。

CalcAもCalcBも、「全く同一のもの」になってしまうのです。

ええ?!どうして?? これがポインタの効果なんです(汗)

ヒープ領域をもっと詳しく見てみると、こういうことになっています。

f:id:maxminkun:20161030161557j:plain

ヒープ領域に確保されたものは可変なので、どこからどこまでが確保した場所!と明示的に言うことが出来ません。だって、大きさが変わっている可能性があるからです。

ですから、ここに確保されたものは、メモリの番地を使ってやり取りすることになっています。

メモリのどこに場所を確保したか。そして、確保した場所の先頭の番地を覚えておきます。

例えば、CalcAをインスタンス生成した際には、実際にメモリ上に確保した領域と、その領域の先頭の番地(例では001)を覚えておく場所があります。

そして、普段はこの番地を使ってやり取りします。これには理由があります。一つは、先ほど挙げたように、確保する領域が可変であるためです。

もう一つの理由としては、代入するという行為は、代入するものをまた別の場所に一旦確保して入れ込むという作業なのです。つまり、先ほどの変数の場合の「C = A + B;」は、AとBと同じ大きさのモノを別に用意して、コピーして計算しています。これを可変のモノに対して同様に行うと、もしかするとその対象の物はメチャクチャにでっかいものかも知れません。コピーを用意するだけで一苦労ですし、最悪メモリが足りないかもしれません。(足りなくなる時は相当な大きさの時だけですけど・・・)

ですから、あくまで先頭番地という簡単な数字をやり取りすることで済まそうとしたのです。

これが所謂、ポインタと呼ばれるものです。

と言うわけで、「CalcA = CalcB;」ということをすると以下のことが発生します。(ClacBの番地をCalcAの番地として上書きする(代入))

f:id:maxminkun:20161030163111j:plain

この原理が分かっていないと、知らず知らずのうちに、クラスが別のインスタンス生成したクラスを扱っていることになったり、意図せずにクラスの内部を書き換えてしまったということになります。

仮にこれが発生しても、こういう原理であることが分かっていれば対応が可能です。これを知らないと、なかなか原因にたどり着きません。

昔のCと言った言語は、これら明示的にポインタの文法が存在し、メモリの番地をガリガリに使い倒しました。

しかしこれによって、メモリのオーバーフローを起こしたり、メモリの意図せずの領域書き換えなどのリスクも多かったのです。

現在は、Java、C#と言った言語は、表上はポインタはいなくなりました。しかし、本当はポインタは存在していて、ポインタの原理を理解しないと、プログラムが正常動作しないということに頭を抱えることになります。


まとめ

さて、今回はかなりボリュームがあったと思います。

簡単にまとめると以下になります。

  • メモリにはスタック領域とヒープ領域がある
  • 変数はスタック領域に確保される
  • 変数以外のものは基本的にヒープ領域に確保される。
  • ヒープ領域に確保されたものは、番地(ポインタ)でやりとりが行われるので、注意が必要である。

と言ったところでしょうか。

かなり大きな山ですので、一度では理解が難しいかもしれません。

その場合はご質問を、ここでもtwitterでもいただければ、いつでも解説を致します。

それでは楽しい、プログラミングライフを!!

adios!!

www.milkmemo.com