ここでは、仮想関数の動的結合のメカニズムについて
説明します。V-Tableについて知らなくてもプログラムは書けますので、読み飛ばしていただいても
何の問題もありません。
#include <iostream>
using namespace std;
class Base{
public:
long l;
virtual void func1(){
cout << "Base:func1()" << endl;
}
};
class Deriv : public Base{
public:
virtual void func1(){
cout << "Deriv:func1()" << endl;
}
};
void main(){
Base* p = new Deriv();
p->func1();
}
|
Deriv:func1()
|
上記のようなプログラムでpは、Baseクラス型ポインタであるのに、その
ポインタが指しているインスタンスはDerivクラスです。したがってDerivクラスの
func1が呼ばれます。これを動的結合といいました。
動的結合はインスタンスの中身を調べるために、変数の型(この場合Baseクラスのポインタ型)
では当然判断できません。ということは、インスタンスの実態に何か細工がしているはずです。
そこで、仮想関数を使った場合とそうでない場合のインスタンスのサイズを見てみましょう。
非仮想関数 | 仮想関数 |
#include <iostream>
using namespace std;
class A{
public:
long l;
void func1(){
cout << "A:func1()" << endl;
}
};
void main(){
cout << sizeof(A) << endl;
}
|
#include <iostream>
using namespace std;
class A{
public:
long l;
virtual void func1(){
cout << "A:func1()" << endl;
}
};
void main(){
cout << sizeof(A) << endl;
}
|
4
|
8
|
左と右では仮想関数か、そうでないかの違いしかないのに、サイズが4バイト異なっています。
この4バイトが、上で述べた「細工」なのです。この4バイトはV-Tableへのポインタです。
では、V-Tableとは何なのでしょうか。V-Tableとプログラムが起動する順番などを
説明します。
#include <iostream>
using namespace std;
class Base{
public:
long l;
virtualvoid v_func1(){
:
:
}
virtualvoid v_func2(){
:
:
}
void func3(){
:
:
}
};
|
class Deriv : public Base{
public:
virtual void func1(){
:
:
}
};
void main(){
Base*p = new Deriv();
p->func1();
p->func2();
}
|
上記のようなプログラムがあったとします。まず、このプログラムを実行しようとすると、
プログラム領域が適当なアドレスに割り当てられます。ここでは仮に下図のように、
「Base::v_func1()」が1000番地、「Base::v_func2()」が2000番地・・・、に配置されたとします。
次にV-Tableが作られます。V-Tableは各クラスの仮想関数と実際に配置されたアドレスとの
対応表のようなものです。ここでは仮に下図のように、Baseクラス用のV-Tableが10000番地に、
Derivクラス用のV-Tableは11000番地に配置されたとします。V-Tableには非仮想関数は含まれません。
また、V-Tableはクラスに1つで、そのインスタンスが複数作られてもV-Tableは1つしか
存在しません。
ここまではプログラムがOSによってロードされた時点で行われ、実行開始前のできごとです。
さて、いよいよ実行です。仮にDerivクラスのインスタンスが22000番地に作成されたとします。
Derivクラスのメンバー変数はlong型のl1つです。それに加えて、Derivクラス用のV-Table
を指すポインタが含まれるので、sizeof(Deriv)は計8バイトになります。
ここで、「p->v_func2()」を実行したとします。まず、V-Tableを参照して、
v_func2()が2000番地から始まっていることを知ります。そして2000番地にジャンプすることにより
「Base::v_func2()」が実行されます。
次に、「p->v_func1()」を実行したとします。まず、V-Tableを参照して、
v_func1()が4000番地から始まっていることを知ります。そして4000番地にジャンプすることにより
「Deriv::v_func1()」が実行されます。
では、「p->func3()」を実行した場合はどうでしょうか。func3は仮想関数
ではないので、動的結合ではなく、コンパイル時にどの関数を実行するかを決定する静的結合
になります。したがって、V-Tableを参照することなく、3000番地すなわち「Base::func3()」
にジャンプするようにコンパイルされます。