C++OpenSiv3D入門講座 Vol. 10 基底・派生クラス・仮想関数・ポリモーフィズム

C++OpenSiv3D入門講座

基底クラスと派生クラス

クラスを継承することで、既存のクラスの要素を引き継いだ新しいクラスを作成することができる。

継承元のクラスを基底クラス、継承先のクラスを派生クラスという。

派生クラスは基底クラスのメンバを持つ。

下の例の場合DerivedクラスがBaseクラスを継承している。

#include <iostream>

class Base {
public:
    Base(int _x) :
        x{ _x }
    {
    }
    int x;
    void show() {
        std::cout << "x:" << x << std::endl;
    }
};

class Derived : public Base {
public:
    Derived(int _x) :
        Base{ _x }
    {
    }
};


int main() {

    Base obj_b{ 12 };
    obj_b.show();

    Derived obj_d{ 23 };
    obj_d.show();

    return 0;
}


書き方

class 派生クラス名 : アクセス 基底クラス名{
}

アクセスがpublicだと基本クラスのpublicメンバが派生クラスのpublicメンバに、privateだと基本クラスのpublicメンバが派生クラスのprivateメンバになる。 派生クラスから基底クラスのコンストラクタを呼ぶときは、コンポジションの時と同様にメンバイニシャライザで呼べばよい。

class 派生クラス名 : アクセス 基底クラス名{
public:
    派生クラス名(引数1,引数2, ...):
        基底クラス名(引数1,引数2, ...)
    {
    }
};

基底クラスへのポインタと参照

基底クラスへのポインタ/参照には、派生クラスのポインタ/参照を入れることが出来る。

#include <iostream>

class Base {
public:
    int x;

    Base(int _x) :
        x{ _x }
    {
    }

    void show() {
        std::cout << "show関数が呼ばれました。 xは:" << x << std::endl;
    }
};

class Derived : public Base {
public:
    Derived(int _x) : 
        Base{ _x }
    {
    }

};

int main() {

    Base obj_b{ 100 };
    Derived obj_d{ 200 };

    obj_b.show();
    obj_d.show();

    std::cout << std::endl;

    Base* ptr1 = &obj_b;
    Base* ptr2 = &obj_d; //基底クラスへのポインタに派生クラスのポインタを入れられる

    ptr1->show();
    ptr2->show();

    std::cout << std::endl;

    Base& ref1 = obj_b;
    Base& ref2 = obj_d; //基底クラスの参照に派生クラスの参照を入れられる

    ref1.show();
    ref2.show();

    std::cout << std::endl;

    return 0;
}

仮想関数

基底クラスでvirtualをつけ仮想関数を作り、派生クラスでその関数をoverrideすることで、派生クラスを指している基底クラスへのポインタから、派生クラスでoverrideされた関数を呼び出すことが出来るようになる。 以下は、show関数をオーバーライドしている例。メンバ関数の前にvirtualをつけることで仮想関数を定義できる。また、派生クラスでは、override指定子をつけてオーバーライドを明示しながらshow関数をオーバーライドしている。

#include <iostream>

class Base {
public:
    int x;

    Base(int _x) :
        x{ _x }
    {

    }

    virtual void show() {
        std::cout << "基底クラスのshow関数が呼ばれました。 xは:" << x << std::endl;
    }
};

class Derived : public Base {
public:
    Derived(int _x) :
        Base{ _x }
    {
    }

    void show() override {
        std::cout << "派生クラスのshow関数が呼ばれました。 xは:" << x << std::endl;
    }
};

int main() {

    Base obj_b{ 100 };
    Derived obj_d{ 200 };

    obj_b.show();
    obj_d.show();

    std::cout << std::endl;

    Base* ptr1 = &obj_b;
    Base* ptr2 = &obj_d; // ptr2は、派生クラスを指している基底クラスへのポインタ

    ptr1->show();
    ptr2->show();

    std::cout << std::endl;

    return 0;
}

継承によるポリモーフィズム

例えばシューティングで敵A,敵B,敵Cを作りたいとする。もし、敵A,敵B,敵Cの配列をそれぞれ作って管理しようとすると大変だし、敵が一種類増えるたびに新しい配列を作らなければならない。 下のようにEnemyの基底クラスIEnemyを定義し、それを継承すれば、派生クラスを一つの配列で扱うことが可能になる。

このように、異なる型の実体を同一の型として扱えるようなインターフェースを提供することをポリモーフィズムという。

#include <iostream>
#include <vector>

class IEnemy {
public:
    int x;

    //コンストラクタ
    IEnemy(int _x) :
        x{ _x }
    {
    }

    virtual void show() {
        std::cout << "基底クラスのshow関数が呼ばれました。 xは:" << x << std::endl;
    }
};

class EnemyA : public IEnemy {
public:

    EnemyA(int _x) :
        IEnemy{ _x }
    {
    }

    void show() override {
        std::cout << "EnemyAクラスのshow関数が呼ばれました。 xは:" << x << std::endl;
    }
};


class EnemyB : public IEnemy {
public:

    EnemyB(int _x) :
        IEnemy{ _x }
    {
    }

    void show() override {
        std::cout << "EnemyBクラスのshow関数が呼ばれました。 xは:" << x << std::endl;
    }
};


class EnemyC : public IEnemy {
public:

    EnemyC(int _x) :
        IEnemy{ _x }
    {
    }

    void show() override {
        std::cout << "EnemyCクラスのshow関数が呼ばれました。 xは:" << x << std::endl;
    }
};



int main() {
    std::vector<IEnemy*> vec;

    vec.emplace_back(new EnemyA{ 100 });
    vec.emplace_back(new EnemyB{ 200 });
    vec.emplace_back(new EnemyC{ 300 });

    //違うクラスなのに、同じ配列で扱えた!!!!!
    for (const auto& enemy : vec) {
        enemy->show();
    }

    return 0;
}

純粋仮想関数

純粋仮想関数を作ると、そのクラスはインスタンス化出来なくなる。上の例では敵A,B,Cの基底クラスにIEnemyが定義されているが、このようなクラスをインターフェイスを定義するクラスという。純粋仮想関数はインターフェイスを定義するクラスを作るときに使い、誤ってインターフェイスを定義するクラスを生成してしまうのを防げる。

#include <iostream>

class Base {
public:
    double x;

    Base(int _x) :
        x{ _x }
    {
    }

    virtual void show() = 0; //純粋仮想関数
};

class Derived : public Base {
public:
    Derived(int _x) :
        Base{ _x }
    {
    }

    void show() {
        std::cout << "派生クラスのshowが呼ばれました。 xは:" << x << std::endl;
    }
};

int main() {
    Base* ptr1 = new Derived{ 100 };
    ptr1->show();

    //コメントを外すと純粋仮想関数をもつクラスをインスタンス化してしまうのでコンパイルエラー
    //Base b{ 100 };
    //Base *ptr2 = new Base{ 100 };

    return 0;
}

純粋仮想関数の書き方

virtual 返り値の型 関数名() = 0;
例:virtual void show() = 0;

基底クラスの仮想デストラク

以下の例では、Baseを継承したDerivedクラスを作り、Derivedをnewした後、そのポインタをBaseへのポインタに入れ、その後deleteしている。また、基底クラスBaseで仮想デストラクタを定義、派生クラスDerivedでデストラクタをオーバーライドし定義している。 DerivedクラスをさすBaseクラスのポインタを解放した際、Baseクラスに仮想デストラクタがないと、Baseクラスとしての領域のみ解放されてしまい、Derivedクラスの領域が解放されない。よって、ポリモーフィズムのための基底クラスには仮想デストラクタの宣言が必要である。
基底クラスで仮想デストラクタを宣言する例

#include <iostream>

class Base {
public:
    int x;

    Base(int _x) :
        x{ _x }
    {
    }

    // 仮想デストラクタを定義
    virtual ~Base() {
        std::cout << "仮想デストラクタ~Baseが呼ばれました。" << std::endl;
    };
};

class Derived : public Base {
public:
    Derived(int _x) :
        Base{ _x }
    {
    }

    // デストラクタをオーバーライドし定義
    ~Derived() {
        std::cout << "デストラクタ~Derivedが呼ばれました。" << std::endl;
    };
};


int main() {

    Base* ptr = new Derived{ 10 };
    delete ptr;

    return 0;
}

デストラクタの中身が特にない場合は、以下のようにdefaultをつけてデストラクタの中身の生成をコンパイラに任せることができる。また、派生クラスのデストラクタは自動生成されるもので問題ないので、書かなくてもよい。

#include <iostream>

class Base {
public:
    int x;

    Base(int _x) :
        x{ _x }
    {
    }

    // 仮想デストラクタを定義
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    Derived(int _x) :
        Base{ _x }
    {
    }

    // デストラクタをオーバーライドし定義
    ~Derived() = default;
};


int main() {

    Base* ptr = new Derived{ 10 };
    delete ptr;

    return 0;
}

Tips:override指定子

実は、override指定子がなくてもオーバーライドすることはできる。しかし、override指定子を用いてオーバーライドを明示することで、コンパイラにoverrideしていることを明示し、ミスを防ぐことができる。 以下は、show関数を派生クラスでオーバーライドするつもりだったが、showaとミスタイプしてしまった例。override指定子を用いてoverrideを明示しているが、showaという名前の仮想関数はないので、コンパイルしようとするとコンパイルエラーになり、オーバーライドしそびれていることに早い段階で気づけるようになっている。

// このソースコードはコンパイル不可
#include <iostream>

class Base {
public:
    virtual void show() {
        std::cout << "基底クラスのshow関数が呼ばれました。" << std::endl;
    }
};

class Derived : public Base {
public:
    // 本当はshowとタイプするつもりだったが、showaとミスタイプしてしまった例。コンパイルできない。
    void showa() override {
        std::cout << "派生クラスのshow関数が呼ばれました。" << std::endl;
    }
};

int main() {
    Base base;
    Derived derived;

    base.show();
    derived.show();

    return 0;
}

Tips:宣言と定義を分ける場合のoverrideの書き方

以下の例はDerivedクラス内でshow関数をオーバーライドした例。override指定子はクラス内の関数宣言(.hのほう)にはつけるが、関数定義(cppのほう)にはつけない。

class Derived : public IBase {
public:
    void show() override;
};
void Derived::show() {
    std::cout << "派生クラスのshow関数が呼ばれました。" << std::endl;
}

まとめ

  • クラスを継承することで、既存のクラスの要素を引き継いだ新しいクラスを作成することができる
  • 継承元のクラスを基底クラス、継承先のクラスを派生クラスという
  • 基底クラスへのポインタ/参照には、派生クラスのポインタ/参照を入れることが出来る
  • 基底クラスでvirtualをつけ仮想関数を作り、派生クラスでその関数をoverrideすることで、基底クラスへのポインタから、派生クラスでoverrideされた関数を呼び出すことが出来るようになる
  • 異なる型の実体を同一の型と同じように扱えるようなインターフェースを提供することをポリモーフィズムという
  • 純粋仮想関数を作ると、誤ってインターフェイスを定義するクラスを生成してしまうのを防げる
  • 基底クラスには仮想デストラクタを作成しよう
  • override指定子は省略できるが、ミスを防ぐために書こう
  • override指定子はクラス内の関数宣言(.hのほう)にのみつければ良い。

演習問題(コンソール)

  1. 以下の様なクラスを作った。Dog,Catを参考に、IAnimalクラスを継承したクラスをもう1つ作れ。
    # include<iostream>
    class IAnimal {
    public:
        double weight;//重さ
        IAnimal(double w) :
            weight{ w }
        {
        }
        virtual void talk() {
            std::cout << "基底クラスのtalk関数が呼ばれました。 重さは:" << weight << std::endl;
        }
        virtual ~IAnimal() = default;
    };
    class Dog : public IAnimal {
    public:
        Dog(double w) :
            IAnimal{ w }
        {
        }
        void talk() override {
            std::cout << "わんわん 重さは:" << weight << std::endl;
        }
    };
    class Cat : public IAnimal {
    public:
        Cat(double w) :
            IAnimal{ w }
        {
        }
        void talk() override {
            std::cout << "ネコチャン 重さは:" << weight << std::endl;
        }
    };
    int main() {
    }
    
  2. IAnimal型へのポインタを要素に持つvectorにnewで動的確保したIAnimalの派生クラスへのポインタを適当に複数入れた後、すべてのインスタンスtalk関数を呼び出せ。
  3. IAnimalのtalk関数を純粋仮想関数にせよ。

演習問題(OpenSiv3D)

  1. vol.8の演習問題(OpenSiv3D)のプログラムを書き換え、継承によるポリモーフィズムを用いて、数種類の敵を作りなおせ。

演習問題の拡張

これまでやったことを用いて、演習問題(OpenSiv3D)のプログラムを拡張しよう。


拡張例

  • プレイヤーが弾を撃てるようにする
  • 敵が弾を撃ってくるようにする
  • プレイヤーの弾で敵が倒れ、敵の弾でプレイヤーが倒れるようにする
  • 倒れた時にエフェクトを出す
  • スコアを表示する


また、拡張例のようなものを作りたい場合のクラス構成の例を、以下に示す。

クラス構成の例
GameManagerクラス(他のクラスを所持し、管理する)
|- Playerクラス
|- PlayerBulletManagerクラス(PlayerBulletクラスのvectorを所持)
|- EnemyManagerクラス(Enemyクラスのvectorを所持)
|- EnemyBulletManagerクラス(EnemyBulletクラスのvectorを所持)
|- EffectManagerクラス(Effectクラスのvectorを所持)
|- ScoreManagerクラス(スコアを管理するクラス)

当たり判定やエフェクト等については、OpenSiv3Dで便利な機能が用意されている。OpenSiv3DのDocumentを見てみよう。

zenn.dev

Documentの例はスマートな書き方をしていてC/C++初級者には難しいものもあるかもしれない。分からなくなったら是非周りの人に聞いてみたり、勉強と思って頑張って調べながら読んでみたりしよう。

C++OpenSiv3D入門講座