動く側から見たオブジェクト指向

Java や C++ はオブジェクト指向の言語です。クラスとかオブジェクトとかは、大抵プログラムを書く側から説明されますが、実行する側(コンピューター側)のことを知っていると、「なんでこうなっているのか」がよく分かります。ここでは、メモリの使い方から、オブジェクト指向を裏から説明してみます。

復習: コンピューターの構成

まずは話を進める前に、コンピューターの仕組みを復習しておきます。 以下の 3 つがあれば、コンピューターだと言えます。

これらは、パソコンなら別々の部品ですし、組み込み機器だと一つのチップに 入ってしまっている場合もあります。 メモリには「アドレス」(番地) が振られていて、アドレスを指定してデータを読み書きできます。アドレスは C 言語だとお馴染み、ポインタの値です。(ただし、仮想記憶がある OS では間にアドレス変換が入ります)

関数もメモリに乗っている

メモリの役割は、CPU で計算するものやした結果を保存しておくことです。 C 言語でいう変数はメモリに乗っていて、変数ごとにメモリアドレスがあります。
さて、プログラム実行中にメモリに乗っているものは変数だけではありません。関数 (プログラム自体)も、変数と同じようにメモリに乗っています。メモリに乗っているということは、関数にもメモリアドレスがあるということです。

これは C のプログラムだと、以下のように簡単に確認できます。
int a(){
}

int main(void){
  printf("%x\n", &a);
  return 0;
}
& でアドレスを表示するのは、変数のときと同じルールです。
このプログラムを実行すると、関数 a のアドレスが表示されます。
80484b0
(この話はもっと進められるのですが、これは Java の記事なのでこのへんで止めておきます)

オブジェクトとメモリ

C 言語の場合、メモリに乗っているのは「変数と関数」でした。 さて、Java の場合はどうでしょうか?
Java の場合、変数とメソッド(関数)は、どちらもオブジェクトに属しています。 なので、メモリに乗っかっているときには、まずオブジェクトを作って、 そこに変数とメソッドが乗っていることになります。
ここで、Java ではインスタンスとクラス定義が別のオブジェクトになっているのが ポイントです。 図にするとこんな感じです。

この例では、Point というクラスの p0 というインスタンスがあります。 Point のメソッド getX() は、クラスの側にいます。 一方、変数 mx や mY はインスタンスの側にいます。 さらに、インスタンスからクラス定義に向かってポインタが伸びています。 これよって、インスタンスはどのクラスに属するかを知ることができます。
さらにインスタンスを増やしてみた例を載せます。

この例では、p0 に加えてインスタンス p1 も登場しましたが、 クラス定義は 1 つのままです。 p0 と p1 は同じクラス定義を指しています。

static と継承

さっきの「クラスとインスタンスがメモリに乗っている」図が頭に浮かぶようになると、static とか継承とか、オブジェクト指向の基本的な概念もすっきり分かるようになります。まずはstatic です。

ここでは、mScale という static な変数が追加されています。ここで、static な変数は普通の変数とは格納される場所が違って、インスタンスではなくクラス(紺色)に入ります。なので、インスタンスが複数あっても、static 変数は一つしかないことになります。

次は継承です。

継承の場合は、クラスからスーパークラスにポインタが伸びています。 これにより、親クラス(スーパークラス)をたどることができます。

this ポインタ

ここまでで、メモリの中にどんな風に変数やメソッドが乗っているかの説明ができました。では、実際に実行されるときのことを考えてみます。

getX() の実行中を考えてみます。ここの命令が一行ずつ実行されるとき、mX というのはインスタンス側の変数です。一方、メソッドが乗っているのはクラス側です。この関数とインスタンス変数は全然別の場所にある状況で、どうやって mX にたどり着けるんでしょうか?

答えはコンパイル時に行われている、ちょっとした処理(ズル)にあります。getX() は引数が無い関数のように見えますが、コンパイルするときに、ポインタを一つ引数に取るようになっています。このポインタは、メソッドを呼び出すときに、呼び出したインスタンスのアドレスが入るようになっています。このポインタのことを this ポインタと呼びます。
this ポインタが引数に追加されたのに合わせて、メソッド内での変数にも全て "this." のように、this ポインタ経由でインスタンス変数をたどれるような変換がされます。これにより、めでたくメソッド内からインスタンスにアクセスできるようになっています。

Java には普通のメソッドのほかに、static メソッドというものもあります。 こちらはどうでしょうか?

static メソッドは、インスタンス変数には一切アクセスできず、クラス変数だけにアクセスできます。なので、this ポインタをもらう必要はありません。この例では、mX や mY にはアクセスできず、sScale だけにアクセスできます。つまり、C の関数とほとんど同じようなものです。

なぜ Java の main() は static なのか

ここまで説明すると、なぜ Java の main() が static なのかの説明ができます。 どんな言語でも、実行されたときにどこが最初に呼ばれるかはルールが必要です。 C では main() という関数が呼ばれる、というルールになっています。

Java でも同じようにしたかったのですが、Java ではすべての関数はクラスに属することになっています。なのでクラスのメソッドにしたのですが、main() はプログラムの開始場所を指定したいだけなので、this が無くても呼べる static メソッドにしたわけです。