読者です 読者をやめる 読者になる 読者になる

オブジェクト指向におけるポリモーフィズムについて

ポリモーフィズムに関する動作をどうすれば機械的に理解できるのかを考えていて何度も失敗して今に至るのだが、今回こそはと絵を作成してみた。JavaC++(ほぼ同じ)でコードは絵と対応付けて実装している。

Javaであれば、private関数以外はサブクラスにオーバーライドされるし、C++であれば、virtualメンバ関数はサブクラスのオーバーライドされる。つまり、スーパークラスの参照変数・ポインタ変数に、サブクラスのインスタンスを代入して使うべきもの(これこそがポリモーフィズム)であるが、以下は比較のためにサブクラスの参照変数・ポインタ変数を使っている場合も実装している。

Java

f:id:nishidy:20150808211530p:plain

$ 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++

f:id:nishidy:20150808211544p:plain

$ 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=&sub;
    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