[C++] クラスの関数をオーバーライドする方法

C++でクラスの関数をオーバーライドするには、基底クラスで仮想関数virtualとして宣言された関数を派生クラスで再定義します。

派生クラスで関数をオーバーライドする際には、関数のシグネチャ(引数や戻り値の型)が基底クラスのものと一致している必要があります。

C++11以降では、オーバーライドする関数にoverrideキーワードを付けることで、意図的なオーバーライドであることを明示し、間違いを防ぐことができます。

この記事でわかること
  • オーバーライドの基本
  • overrideキーワードの重要性
  • 抽象クラスと純粋仮想関数の利用
  • 多重継承における注意点
  • スマートポインタとの組み合わせ方法

目次から探す

オーバーライドとは何か

オーバーライドとは、C++において基底クラスで定義された仮想関数を、派生クラスで再定義することを指します。

これにより、派生クラスのオブジェクトが基底クラスのポインタや参照を通じて呼び出された場合でも、派生クラスで定義された関数が実行されるようになります。

オーバーライドは、ポリモーフィズムを実現するための重要な機能であり、異なるクラス間での一貫したインターフェースを提供します。

これにより、コードの再利用性や拡張性が向上し、柔軟なプログラミングが可能になります。

C++でのオーバーライドの基本

基底クラスでの仮想関数の宣言

基底クラスでオーバーライドを行うためには、まず仮想関数を宣言する必要があります。

仮想関数は、virtualキーワードを用いて定義され、派生クラスで再定義されることを前提としています。

以下は、基底クラスでの仮想関数の宣言の例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() { // 仮想関数の宣言
        cout << "Base class show function" << endl;
    }
};

派生クラスでの関数の再定義

派生クラスでは、基底クラスで宣言された仮想関数を再定義することができます。

この再定義により、派生クラスのオブジェクトが基底クラスのポインタを通じて呼び出された場合でも、派生クラスの関数が実行されます。

以下は、派生クラスでの関数の再定義の例です。

class Derived : public Base {
public:
    void show() override { // 関数の再定義
        cout << "Derived class show function" << endl;
    }
};

overrideキーワードの使用

C++11以降、overrideキーワードを使用することで、関数が基底クラスの仮想関数をオーバーライドしていることを明示的に示すことができます。

これにより、誤って関数のシグネチャを変更してしまった場合にコンパイルエラーが発生し、バグを防ぐことができます。

上記の例でもoverrideを使用しています。

オーバーライド時の関数シグネチャの一致

オーバーライドを行う際には、基底クラスの仮想関数と派生クラスの関数のシグネチャが一致している必要があります。

具体的には、関数名、引数の型、引数の数が同じでなければなりません。

戻り値の型は、基底クラスの関数と同じか、派生クラスの関数が戻り値を変えることはできません。

これにより、正しい関数が呼び出されることが保証されます。

オーバーライドとアクセス修飾子の関係

オーバーライドを行う際には、アクセス修飾子にも注意が必要です。

基底クラスの仮想関数がpublicまたはprotectedとして宣言されている場合、派生クラスでのオーバーライドも同様にpublicまたはprotectedでなければなりません。

privateでオーバーライドすることはできず、アクセス修飾子が異なると、オーバーライドとは見なされません。

これにより、クラスの設計が意図した通りに機能することが保証されます。

オーバーライドの具体例

基本的なオーバーライドの例

基本的なオーバーライドの例として、基底クラスAnimalとその派生クラスDogを考えます。

Animalクラスには仮想関数makeSoundがあり、Dogクラスでこの関数をオーバーライドします。

#include <iostream>
using namespace std;
class Animal {
public:
    virtual void makeSound() { // 基底クラスの仮想関数
        cout << "Animal sound" << endl;
    }
};
class Dog : public Animal {
public:
    void makeSound() override { // オーバーライド
        cout << "Woof!" << endl;
    }
};
int main() {
    Animal* animal = new Dog(); // 基底クラスのポインタで派生クラスを指す
    animal->makeSound(); // "Woof!"が出力される
    delete animal;
    return 0;
}
Woof!

overrideキーワードを使った例

overrideキーワードを使用することで、オーバーライドを明示的に示すことができます。

以下の例では、Shapeクラスとその派生クラスCircledraw関数をオーバーライドしています。

#include <iostream>
using namespace std;
class Shape {
public:
    virtual void draw() { // 基底クラスの仮想関数
        cout << "Drawing a shape" << endl;
    }
};
class Circle : public Shape {
public:
    void draw() override { // `override`キーワードを使用
        cout << "Drawing a circle" << endl;
    }
};
int main() {
    Shape* shape = new Circle(); // 基底クラスのポインタで派生クラスを指す
    shape->draw(); // "Drawing a circle"が出力される
    delete shape;
    return 0;
}
Drawing a circle

複数の派生クラスでのオーバーライド

複数の派生クラスで同じ基底クラスの仮想関数をオーバーライドすることも可能です。

以下の例では、Vehicleクラスを基底クラスとして、CarBikeの2つの派生クラスがそれぞれmove関数をオーバーライドしています。

#include <iostream>
using namespace std;
class Vehicle {
public:
    virtual void move() { // 基底クラスの仮想関数
        cout << "Vehicle is moving" << endl;
    }
};
class Car : public Vehicle {
public:
    void move() override { // `Car`クラスでオーバーライド
        cout << "Car is driving" << endl;
    }
};
class Bike : public Vehicle {
public:
    void move() override { // `Bike`クラスでオーバーライド
        cout << "Bike is pedaling" << endl;
    }
};
int main() {
    Vehicle* vehicle1 = new Car(); // `Car`のインスタンス
    Vehicle* vehicle2 = new Bike(); // `Bike`のインスタンス
    vehicle1->move(); // "Car is driving"が出力される
    vehicle2->move(); // "Bike is pedaling"が出力される
    delete vehicle1;
    delete vehicle2;
    return 0;
}
Car is driving
Bike is pedaling

コンストラクタやデストラクタのオーバーライドは可能か?

C++では、コンストラクタやデストラクタをオーバーライドすることはできません。

コンストラクタはクラスのインスタンス化時に呼び出され、デストラクタはインスタンスが破棄される際に呼び出されるため、オーバーライドの概念が適用されません。

ただし、基底クラスのデストラクタを仮想関数として宣言することで、派生クラスのデストラクタが正しく呼び出されるようにすることは可能です。

オーバーライドに関する注意点

オーバーライド時の型の不一致によるエラー

オーバーライドを行う際には、基底クラスの仮想関数と派生クラスの関数のシグネチャが一致している必要があります。

型の不一致があると、コンパイルエラーが発生します。

例えば、基底クラスの関数がint型の引数を持つ場合、派生クラスの関数も同じ型の引数を持たなければなりません。

以下の例では、型の不一致によるエラーが発生します。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void display(int value) { // 基底クラスの仮想関数
        cout << "Base value: " << value << endl;
    }
};
class Derived : public Base {
public:
    void display(double value) { // 型が不一致
        cout << "Derived value: " << value << endl;
    }
};
int main() {
    Base* obj = new Derived();
    obj->display(10); // エラー: display(int)が見つからない
    delete obj;
    return 0;
}

overrideキーワードを使わない場合のリスク

overrideキーワードを使用しない場合、基底クラスの仮想関数をオーバーライドするつもりが、誤って異なるシグネチャの関数を定義してしまうことがあります。

この場合、基底クラスの関数はオーバーライドされず、意図しない動作を引き起こす可能性があります。

以下の例では、overrideを使わないことで、意図しない動作が発生します。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() { // 基底クラスの仮想関数
        cout << "Base class show function" << endl;
    }
};
class Derived : public Base {
public:
    void show(int value) { // オーバーライドではない
        cout << "Derived class show function with value: " << value << endl;
    }
};
int main() {
    Base* obj = new Derived();
    obj->show(); // "Base class show function"が出力される
    delete obj;
    return 0;
}

基底クラスの関数を呼び出す方法(Base::関数名)

派生クラスでオーバーライドした関数内から、基底クラスの関数を呼び出すことができます。

これにより、基底クラスの機能を保持しつつ、派生クラスの特定の処理を追加することが可能です。

以下の例では、Derivedクラスshow関数内から基底クラスのshow関数を呼び出しています。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() {
        cout << "Base class show function" << endl;
    }
};
class Derived : public Base {
public:
    void show() override {
        Base::show(); // 基底クラスの関数を呼び出す
        cout << "Derived class show function" << endl;
    }
};
int main() {
    Base* obj = new Derived();
    obj->show(); // "Base class show function"と"Derived class show function"が出力される
    delete obj;
    return 0;
}
Base class show function
Derived class show function

オーバーライドとポリモーフィズムの関係

オーバーライドはポリモーフィズムの実現において重要な役割を果たします。

ポリモーフィズムとは、異なるクラスのオブジェクトが同じインターフェースを持ち、同じメソッドを呼び出すことで異なる動作をすることを指します。

オーバーライドを使用することで、基底クラスのポインタや参照を通じて、派生クラスの関数を呼び出すことができ、柔軟なプログラミングが可能になります。

これにより、コードの再利用性や拡張性が向上し、より効率的な設計が実現されます。

C++11以降のオーバーライドの新機能

overrideキーワードの導入

C++11では、overrideキーワードが導入され、派生クラスで基底クラスの仮想関数をオーバーライドする際に使用できるようになりました。

このキーワードを使用することで、関数が基底クラスの仮想関数を正しくオーバーライドしていることを明示的に示すことができます。

もしシグネチャが一致しない場合、コンパイラはエラーを報告します。

以下の例では、overrideを使用したオーバーライドの例を示します。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void display() {
        cout << "Base class display function" << endl;
    }
};
class Derived : public Base {
public:
    void display() override { // `override`キーワードを使用
        cout << "Derived class display function" << endl;
    }
};
int main() {
    Base* obj = new Derived();
    obj->display(); // "Derived class display function"が出力される
    delete obj;
    return 0;
}
Derived class display function

finalキーワードによるオーバーライドの制限

C++11では、finalキーワードを使用することで、特定の関数がオーバーライドされることを防ぐことができます。

これにより、基底クラスの関数をオーバーライドできないように制限することができます。

以下の例では、Baseクラスshow関数finalを指定し、Derivedクラスでのオーバーライドを禁止しています。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() final { // `final`キーワードを使用
        cout << "Base class show function" << endl;
    }
};
class Derived : public Base {
public:
    void show() { // エラー: `show`はオーバーライドできない
        cout << "Derived class show function" << endl;
    }
};
int main() {
    Base* obj = new Derived();
    obj->show(); // "Base class show function"が出力される
    delete obj;
    return 0;
}

noexceptとオーバーライドの関係

C++11以降、関数にnoexcept指定子を追加することができ、例外を投げないことを明示的に示すことができます。

オーバーライドする際、基底クラスの関数がnoexceptである場合、派生クラスのオーバーライド関数もnoexceptでなければなりません。

以下の例では、noexceptを使用したオーバーライドの例を示します。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void process() noexcept { // `noexcept`指定
        cout << "Base class process function" << endl;
    }
};
class Derived : public Base {
public:
    void process() noexcept override { // `noexcept`を維持
        cout << "Derived class process function" << endl;
    }
};
int main() {
    Base* obj = new Derived();
    obj->process(); // "Derived class process function"が出力される
    delete obj;
    return 0;
}
Derived class process function

constexpr関数のオーバーライド

C++11以降、constexpr関数を定義することができ、コンパイル時に評価される関数を作成できます。

constexpr関数もオーバーライドすることが可能で、基底クラスのconstexpr関数を派生クラスでオーバーライドすることができます。

以下の例では、constexpr関数のオーバーライドを示します。

#include <iostream>
using namespace std;
class Base {
public:
    virtual constexpr int calculate(int x) const { // `constexpr`関数
        return x + 1;
    }
};
class Derived : public Base {
public:
    constexpr int calculate(int x) const override { // オーバーライド
        return x * 2;
    }
};
int main() {
    Base* obj = new Derived();
    cout << obj->calculate(5) << endl; // "10"が出力される
    delete obj;
    return 0;
}
10

オーバーライドの応用例

抽象クラスと純粋仮想関数のオーバーライド

抽象クラスは、少なくとも1つの純粋仮想関数を持つクラスであり、インスタンス化することはできません。

派生クラスはこの純粋仮想関数をオーバーライドする必要があります。

以下の例では、Shapeクラスが抽象クラスであり、draw関数が純粋仮想関数として定義されています。

#include <iostream>
using namespace std;
class Shape {
public:
    virtual void draw() = 0; // 純粋仮想関数
};
class Circle : public Shape {
public:
    void draw() override { // オーバーライド
        cout << "Drawing a circle" << endl;
    }
};
int main() {
    Shape* shape = new Circle(); // 派生クラスのインスタンス
    shape->draw(); // "Drawing a circle"が出力される
    delete shape;
    return 0;
}
Drawing a circle

インターフェースとしての仮想関数の利用

C++では、仮想関数を使用してインターフェースを定義することができます。

インターフェースは、異なるクラスが同じメソッドを持つことを保証し、ポリモーフィズムを実現します。

以下の例では、Animalインターフェースを定義し、DogCatクラスがこのインターフェースを実装しています。

#include <iostream>
using namespace std;
class Animal {
public:
    virtual void speak() = 0; // 純粋仮想関数
};
class Dog : public Animal {
public:
    void speak() override { // オーバーライド
        cout << "Woof!" << endl;
    }
};
class Cat : public Animal {
public:
    void speak() override { // オーバーライド
        cout << "Meow!" << endl;
    }
};
int main() {
    Animal* animals[2];
    animals[0] = new Dog();
    animals[1] = new Cat();
    for (int i = 0; i < 2; i++) {
        animals[i]->speak(); // 各動物の鳴き声を出力
    }
    for (int i = 0; i < 2; i++) {
        delete animals[i];
    }
    return 0;
}
Woof!
Meow!

多重継承とオーバーライドの衝突

C++では多重継承が可能ですが、基底クラスに同名の仮想関数が存在する場合、オーバーライドの衝突が発生することがあります。

この場合、どの基底クラスの関数をオーバーライドするかを明示的に指定する必要があります。

以下の例では、Base1Base2の両方にshow関数があり、Derivedクラスでのオーバーライドを示しています。

#include <iostream>
using namespace std;
class Base1 {
public:
    virtual void show() {
        cout << "Base1 show function" << endl;
    }
};
class Base2 {
public:
    virtual void show() {
        cout << "Base2 show function" << endl;
    }
};
class Derived : public Base1, public Base2 {
public:
    void show() override { // オーバーライド
        Base1::show(); // Base1のshowを呼び出す
        Base2::show(); // Base2のshowを呼び出す
    }
};
int main() {
    Derived obj;
    obj.show(); // "Base1 show function"と"Base2 show function"が出力される
    return 0;
}
Base1 show function
Base2 show function

テンプレートクラスでのオーバーライド

C++では、テンプレートクラスを使用して、型に依存しないクラスを作成することができます。

テンプレートクラス内で仮想関数を定義し、派生クラスでオーバーライドすることも可能です。

以下の例では、テンプレートクラスBaseを定義し、Derivedクラスでオーバーライドしています。

#include <iostream>
using namespace std;
template <typename T>
class Base {
public:
    virtual void display(T value) {
        cout << "Base class value: " << value << endl;
    }
};
class Derived : public Base<int> { // int型のテンプレートを使用
public:
    void display(int value) override { // オーバーライド
        cout << "Derived class value: " << value * 2 << endl;
    }
};
int main() {
    Base<int>* obj = new Derived();
    obj->display(5); // "Derived class value: 10"が出力される
    delete obj;
    return 0;
}
Derived class value: 10

オーバーライドとスマートポインタの組み合わせ

C++では、スマートポインタを使用することで、メモリ管理を簡素化できます。

オーバーライドされた関数を持つクラスをスマートポインタで管理することができます。

以下の例では、std::unique_ptrを使用して、オーバーライドされた関数を持つクラスのインスタンスを管理しています。

#include <iostream>
#include <memory> // スマートポインタ用
using namespace std;
class Base {
public:
    virtual void show() {
        cout << "Base class show function" << endl;
    }
    virtual ~Base() {} // 仮想デストラクタ
};
class Derived : public Base {
public:
    void show() override { // オーバーライド
        cout << "Derived class show function" << endl;
    }
};
int main() {
    unique_ptr<Base> obj = make_unique<Derived>(); // スマートポインタで管理
    obj->show(); // "Derived class show function"が出力される
    return 0;
}
Derived class show function

よくある質問

overrideキーワードは必須ですか?

overrideキーワードは必須ではありませんが、使用することが推奨されます。

このキーワードを使用することで、派生クラスの関数が基底クラスの仮想関数をオーバーライドしていることを明示的に示すことができます。

これにより、シグネチャの不一致によるバグを防ぐことができ、コードの可読性が向上します。

もしoverrideを使用しない場合、誤って異なるシグネチャの関数を定義してしまうと、基底クラスの関数はオーバーライドされず、意図しない動作を引き起こす可能性があります。

オーバーライドとオーバーロードは同時に使えますか?

はい、オーバーライドとオーバーロードは同時に使用できます。

オーバーライドは基底クラスの仮想関数を派生クラスで再定義することを指し、オーバーロードは同じ関数名で異なる引数の型や数を持つ関数を定義することを指します。

したがって、同じクラス内でオーバーライドされた関数とオーバーロードされた関数を共存させることが可能です。

以下の例では、show関数がオーバーライドされ、異なる引数を持つオーバーロードも定義されています。

class Base {
public:
    virtual void show() {
        cout << "Base class show function" << endl;
    }
};
class Derived : public Base {
public:
    void show() override { // オーバーライド
        cout << "Derived class show function" << endl;
    }
    void show(int value) { // オーバーロード
        cout << "Derived class show function with value: " << value << endl;
    }
};

オーバーライドされた関数を基底クラスから呼び出す方法は?

オーバーライドされた関数を基底クラスから呼び出すには、派生クラスのオーバーライド関数内で基底クラスの関数を明示的に呼び出すことができます。

これには、Base::関数名という形式を使用します。

以下の例では、Derivedクラスshow関数内から基底クラスのshow関数を呼び出しています。

class Base {
public:
    virtual void show() {
        cout << "Base class show function" << endl;
    }
};
class Derived : public Base {
public:
    void show() override {
        Base::show(); // 基底クラスの関数を呼び出す
        cout << "Derived class show function" << endl;
    }
};

このようにすることで、基底クラスの機能を保持しつつ、派生クラスの特定の処理を追加することができます。

まとめ

この記事では、C++におけるオーバーライドの基本から具体的な実装例、注意点、応用例まで幅広く解説しました。

オーバーライドは、ポリモーフィズムを実現するための重要な機能であり、クラス間の柔軟なインターフェースを提供することで、コードの再利用性や拡張性を向上させます。

これを踏まえ、実際のプログラミングにおいてオーバーライドを積極的に活用し、より効率的で保守性の高いコードを目指してみてください。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

関連カテゴリーから探す

  • URLをコピーしました!
目次から探す