オブジェクト指向におけるポリモーフィズムについて
ポリモーフィズムに関する動作をどうすれば機械的に理解できるのかを考えていて何度も失敗して今に至るのだが、今回こそはと絵を作成してみた。JavaとC++(ほぼ同じ)でコードは絵と対応付けて実装している。
Javaであれば、private関数以外はサブクラスにオーバーライドされるし、C++であれば、virtualメンバ関数はサブクラスのオーバーライドされる。つまり、スーパークラスの参照変数・ポインタ変数に、サブクラスのインスタンスを代入して使うべきもの(これこそがポリモーフィズム)であるが、以下は比較のためにサブクラスの参照変数・ポインタ変数を使っている場合も実装している。
Java
$ cat Base.java class Base{ public static void main(String[] args){ Base base = new Base(); System.out.println("(Base*)Base func1"); base.func1(); Sub sub = new Sub(); System.out.println("(Sub*)Sub func1"); sub.func1(); SSub ssub = new SSub(); System.out.println("(SSub*)SSub func1"); ssub.func1(); Base basesub = new Sub(); System.out.println("(Base*)Sub func1"); basesub.func1(); Base basessub = new SSub(); System.out.println("(Base*)SSub func1"); basessub.func1(); System.out.println("(Base*)Sub func2"); basesub.func2(); System.out.println("(Sub*)Sub func2"); sub.func2(); System.out.println("(SSub*)SSub func2"); ssub.func2(); } void func1(){ System.out.println("Base:func1"); func2(); } private void func2(){ System.out.println("Base:func2"); func3(); } void func3(){ System.out.println("Base:func3"); } } class Sub extends Base{ void func2(){ System.out.println("Sub:func2"); func3(); } void func3(){ System.out.println("Sub:func3"); } } class SSub extends Sub{ void func3(){ System.out.println("SSub:func3"); } } $ java Base (Base*)Base func1 Base:func1 Base:func2 Base:func3 (Sub*)Sub func1 Base:func1 Base:func2 Sub:func3 (SSub*)SSub func1 Base:func1 Base:func2 SSub:func3 (Base*)Sub func1 Base:func1 Base:func2 Sub:func3 (Base*)SSub func1 Base:func1 Base:func2 SSub:func3 (Base*)Sub func2 Base:func2 Sub:func3 (Sub*)Sub func2 Sub:func2 Sub:func3 (SSub*)SSub func2 Sub:func2 SSub:func3
(Base*)Sub func2はBaseの参照変数を使ってSubインスタンスのfunc2メソッドを呼んでいる。Baseのfunc2はprivateであり、Subのfunc2ではオーバーライドされない。Baseの参照変数を使っているため、ポリモーフィズムにより、Baseのfunc2メソッドが実行される。 一方(Sub*)Sub func2はBaseの参照変数を使っていないため、Subのfunc2メソッドが実行される。
Baseクラスのfunc2のprivate指定を外した結果が以下になる。(Base*)Sub func2のfunc2メソッドはSubクラスのものが呼ばれている。
(Base*)Base func1 Base:func1 Base:func2 Base:func3 (Sub*)Sub func1 Base:func1 Sub:func2 Sub:func3 (SSub*)SSub func1 Base:func1 Sub:func2 SSub:func3 (Base*)Sub func1 Base:func1 Sub:func2 Sub:func3 (Base*)SSub func1 Base:func1 Sub:func2 SSub:func3 (Base*)Sub func2 Sub:func2 Sub:func3 (Sub*)Sub func2 Sub:func2 Sub:func3 (SSub*)SSub func2 Sub:func2 SSub:func3
Note: アクセス制限
- オブジェクト修飾子のアクセス制限を確認
- -(なし)のアクセス制限はprivateより弱くprotectedより強い
- アクセス制限が同じか弱いメソッドでオーバーライドしなければいけない
アクセス制限弱い ↑ public 全てのクラス protected 同じクラス内、同じパッケージ、サブクラス - 同じクラス内、同じパッケージ private 同じクラス内 ↓ アクセス制限強い
C++
$ cat Base.cpp #include <iostream> using namespace std; class Base{ public: void func1(){ cout<<"Base:func1"<<endl; func2(); } void func2(){ cout<<"Base:func2"<<endl; func3(); } virtual void func3(){ cout<<"Base:func3"<<endl; } }; class Sub: public Base{ public: void func2(){ cout<<"Sub:func2"<<endl; func3(); } void func3(){ cout<<"Sub:func3"<<endl; } }; class SSub: public Sub{ public: void func3(){ cout<<"SSub:func3"<<endl; } }; int main(int argc, char* argv[]){ Base base; cout<<"(Base*)Base func1"<<endl; base.func1(); Sub sub; cout<<"(Sub*)Sub func1"<<endl; sub.func1(); SSub ssub; cout<<"(SSub*)SSub func1"<<endl; ssub.func1(); Base* basep; basep=⊂ cout<<"(Base*)Sub func1"<<endl; basep->func1(); basep=&ssub; cout<<"(Base*)SSub func1"<<endl; basep->func1(); cout<<"(Base*)Sub func2"<<endl; basep->func2(); cout<<"(Sub*)Sub func2"<<endl; sub.func2(); cout<<"(Sub*)SSub func2"<<endl; ssub.func2(); } $ ./a.out (Base*)Base func1 Base:func1 Base:func2 Base:func3 (Sub*)Sub func1 Base:func1 Base:func2 Sub:func3 (SSub*)SSub func1 Base:func1 Base:func2 SSub:func3 (Base*)Sub func1 Base:func1 Base:func2 Sub:func3 (Base*)SSub func1 Base:func1 Base:func2 SSub:func3 (Base*)Sub func2 Base:func2 SSub:func3 (Sub*)Sub func2 Sub:func2 Sub:func3 (Sub*)SSub func2 Sub:func2 SSub:func3
vtable
- virtualを付けておけばオーバーライドしてくれるので、サブクラスで同じ名前のメソッドを定義したときはオーバーライドしたいときであるという前提であれば、いつもvirtualを付けておけば良いのではないかと思うかもしれないが、virtualメソッドを作るとvftableというテーブルが作られるので、メモリ使用量やメソッド呼び出しの速度に対してはそれなりにオーバーヘッドがかかることになる
- c.f. http://www.yunabe.jp/docs/cpp_virtual_destructor.html
virtualデストラクタ
- ポリモーフィズムを使う場合、基底クラスのデストラクタも必ずvirtualにしておかないと、オブジェクトの実体がサブクラスの場合に、サブクラスのデストラクタが呼ばれず、サブクラスのオブジェクトのメモリが解放されない
$ cat DestructorPractice.cpp #include <iostream> using namespace std; class Base{ public: Base(){cout<<"Base:constructor"<<endl;} ~Base(){cout<<"Base:destructor"<<endl;} }; class Sub: public Base{ public: Sub(){cout<<"Sub :constructor"<<endl;} ~Sub(){cout<<"Sub :destructor"<<endl;} }; class vBase{ public: vBase(){cout<<"vBase:constructor"<<endl;} virtual ~vBase(){cout<<"vBase:destructor"<<endl;} }; class vSub: public vBase{ public: vSub(){cout<<"vSub :constructor"<<endl;} ~vSub(){cout<<"vSub :destructor"<<endl;} }; int main(int argc, char* argv[]){ Base* base = new Sub(); delete base; cout<<endl; vBase* vbase = new vSub(); delete vbase; } $ ./a.out Base:constructor Sub :constructor Base:destructor vBase:constructor vSub :constructor vSub :destructor vBase:destructor