オブジェクトという考え方
プログラムの入門書を開くと、数値や文字列を扱う初歩的なプログラムがまず最初に紹介されています。しかし、実務で実際に作成するソフトウェアにおいては、そんな単純な数値や文字列だけを扱うようなことはありません。もっと大きなデータの単位、つまりオブジェクトという形でデータが表現されています。
オブジェクトというのは、言語によって多少の違いはありますが、ほとんどは構造体やクラスなどによって表現されます。そしてその中身は結局、数値や文字列のようなプリミティブな型の集まりにすぎません。
実体なのか参照なのか
typedef struct _Vector2 {
double x;
double y;
} Vector2;
void main() {
Vector2 vec;
double distance;
vec.x = 3;
vec.y = 4;
distance = sqrt(vec.x * vec.x + vec.y * vec.y);
printf("%f", distance);
}
このようなコード(C言語)があったとき、vecはVector2型の構造体だし、distanceは倍精度浮動小数点型の変数だし、それ以外の何物でもありません。
入門書にはこのような単純なコードが紹介されていますが、実務で実際に作成するソフトウェアにおいては、もっと大量のオブジェクトが登場し、その数も可変(実行時にしかわからない)です。そして、そのオブジェクトの内容をあちこちに移動させたりもします。
例えば、先ほどのコードが下のようになっていたらどうでしょう。
void main() {
Vector2 vec, vecOther;
double distance;
vec.x = 3;
vec.y = 4;
vecOther = vec;
vecOther.x = 5;
vecOther.y = 12;
distance = sqrt(vec.x * vec.x + vec.y * vec.y);
printf("%f", distance);
}
このコードの出力結果は"5″でしょうか? それとも"13″でしょうか?
正解から言えば、答は"5″です。C言語の場合、Vector2型で宣言した構造体vecやvecOtherは構造体の実体なので、vecOther = vecという代入は、実体間での値のコピーを意味します。
変数や構造体が全部実体だと考えれば、宣言した数と名前でしか実体は存在しません。
宣言した分だけしか変数は存在せず、それらはvecとかvecOtherとかdistanceとかの宣言したときの名前でしか扱われることがありません。
しかし、少しプログラムの規模が大きくなると、これでは不便になってきます。そこで、オブジェクトを実体ではなく参照によって扱う必要性が出てきます。
void main() {
Vector2 *pVec, *pVecOther;
double distance;
pVec = (Vector2 *)malloc(sizeof(Vector2));
pVec->x = 3;
pVec->y = 4;
pVecOther = pVec;
pVecOther->x = 5;
pVecOther->y = 12;
distance = sqrt(pVec->x * pVec->x + pVec->y * pVec->y);
printf("%f", distance);
free(pVec);
}
この場合の出力結果は"13″になります。なぜなら、pVecやpVecOtherは、ポインタという形で表現されたオブジェクトの参照だからです。
オブジェクトの実体はメモリ上のどこかに無名で存在します。pVecやpVecOtherは、その実体への参照を表わしているに過ぎません。
もはや、「オブジェクトの実体と変数が1対1で必ず存在する」というような世界ではありません。変数がどのように宣言されているかに関わらず、オブジェクトはメモリ割り当て(malloc)によって生成され、メモリ解放(free)によって破棄されるという新しい世界観で、オブジェクトというものを扱わなくてはいけません。
C言語の場合、言語仕様上は"*"が付いているかどうかの違いしかありませんが、その本質は実体を扱っているのか参照を扱っているのかという大きな考え方の違いになります。
現代のプログラミング言語は参照を扱うほうが一般的
現代のプログラミング言語、例えばC#の場合、ほとんどのオブジェクトは基本的に参照型です。
class Vector2
{
public double x;
public double y;
}
static void main()
{
Vector2 vec, vecOther;
double distance;
vec = new Vector2();
vec.x = 3;
vec.y = 4;
vecOther = vec;
vecOther.x = 5;
vecOther.y = 12;
distance = Math.Sqrt(vec.x * vec.x + vec.y * vec.y);
System.Console.WriteLine("{0}", distance);
}
このC#のコードの出力結果は"13″です。vecOther = vecという代入は値のコピーではなく、参照先の変更を表わしているからです。
このように、現代の多くのプログラミング言語は、オブジェクトを参照として扱うことのほうが基本的であるとされています。
新しい言語を学ぶとき
プログラミング言語というのは、目的や用途によってさまざまなものがあります。どれが優位ということはなく、必要に応じて新しい言語を学ばなければならないこともあります。
そんなとき、まず最初に、その言語の基本的な仕様を一通り確認しましょう。特に、プリミティブ型、配列、オブジェクトなどの表現が実体的であるか参照的であるかをはっきりと把握しましょう。
代入やメソッドの引数として受け渡した場合、それは実体のクローンなのか、それとも参照なのか。これをあいまいな理解のまま進めてしまうと、予期しない解決困難なバグに遭遇してしまいます。
もしそのようなことが入門書に書いてなかった場合は、自分でさっきのような短いコードを書いてみて、実際に実行して確認しましょう。
メモリ管理
C言語の場合、mallocによってメモリを割り当て、freeでメモリを破棄します。オブジェクトを生成した分だけしっかり破棄しないと、無尽蔵にメモリ使用量が増え続けてしまいます。いわゆる、メモリリークです。
C++の場合はnewとdeleteによってメモリの割り当てと破棄を行いますが、自分でメモリ管理をしなくてはいけない点は変わりません。
このメモリ管理はなかなか難しく、昔からずっとプログラマーを悩ませ続けてきました。
COM(Component Object Model)と呼ばれる技術が登場してからは、参照カウンタという形でオブジェクトの自動破棄が実現されるようになりました。そのオブジェクトを参照している(ポインタ)変数が増えれば参照カウンタを1だけ増加させ、参照を解除すれば参照カウンタを1だけ減少させ、参照カウンタが0になればそのオブジェクトはもうどこからも参照されない不要なオブジェクトなので、自動的に解放されるという仕組みです。
しかしながらCOMのC++の実装では、IUnknown::AddRefとIUnknown::Releaseを正しく呼ばないと参照カウンタの増減が行われず、このあたりをおろそかにするとやっぱりメモリリークが起こってしまいます。
言語仕様の中枢にCOMを据えて上手く内部に隠蔽したVisual Basic(6.0)では、AddRefやReleaseを知らなくても内部で適切に参照カウンタが増減され、不要なオブジェクトが然るべきタイミングで自動的に破棄されます。
参照カウンタの仕組みは十分強力なのですが、循環参照という問題は解決できませんでした。AがBを参照し、BがAを参照している場合、AもBも不要になったとしても、お互いの参照カウンタが1のままなので永久に破棄されないという問題が発生します。
そこで、近年以降のプログラミング言語では、ガベージコレクション(Garbage Collection:ゴミ回収)という機構がほぼ搭載されています。
循環参照のようなものも含めて、とにかく不要になったオブジェクトはガベージコレクタの定期巡回によって検知され、いつかのタイミングで自動的に破棄されます。
さきほどのC#のコードでのvec = new Vector2()で生成したオブジェクトは、mainを抜けた時点で不要になり、どこからも到達不能になります。この「到達不能であること」がいつかのタイミングで検知され、そのときにオブジェクトが破棄されます。
ただし、ガベージコレクションという機構は、「いつ」オブジェクトを破棄するかを保証しません。明示的なタイミングで破棄する必要があるときは、オブジェクトごとに用意されたDisposeなどの破棄処理を明示的に実行する必要があります。
オブジェクトの生存
いずれにしても、オブジェクトを実体的ではなく参照的に扱う近年のほとんどのプログラミング言語では、そのオブジェクトがいつまで「生存」しているかに気を配る必要があります。
どこからも参照されずに到達不可能になったらガベージコレクションなどの機構によって自動的に破棄されるのですが、思いもよらないところでオブジェクトへの参照を保持し続けているかもしれません。
そのあたりの「オブジェクトの生存」の把握をおろそかにしていると、実行時間が長引くにつれて動作が重くなり最後には強制終了してしまう不具合を抱え込んだソフトウェアを作ってしまうかもしれません。
このようなメモリリークによる不具合は再現性が低く、解決が困難になることもしばしばあります。しかしお客様は大激怒。出口の見えない修正作業で徹夜の毎日…という恐ろしい末路が待っています。
そういうことを未然に防ぐために、オブジェクトがどのように扱われているか、メモリリークの可能性は無いか、というような基本的なところをまず最初に抑えておくようにしましょう。特に、新しい言語を学ぶ場合は、その言語の深層部分の仕様について、可能な限り理解を深めることが大切だと思います。
ディスカッション
コメント一覧
まだ、コメントがありません