[C++] 基底クラスでのvirtual修飾子の使い方

C++において、基底クラスでvirtual修飾子を使うことで、派生クラスでメソッドをオーバーライドできるようになります。

これにより、基底クラスのポインタや参照を通じて派生クラスのメソッドを呼び出す「動的ポリモーフィズム」が実現されます。

基底クラスのメソッドにvirtualを付けると、派生クラスで同じシグネチャのメソッドを定義した場合、自動的にオーバーライドされます。

この記事でわかること
  • 基底クラスでのvirtual関数の定義方法
  • 派生クラスでのオーバーライドの重要性
  • 動的ポリモーフィズムの実現方法
  • 抽象クラスとインターフェースの活用
  • virtual関数使用時の注意点と影響

目次から探す

基底クラスでのvirtual修飾子の使い方

基底クラスにおけるvirtual関数の定義

C++において、virtual修飾子を使うことで、基底クラスで定義した関数が派生クラスでオーバーライドされることを可能にします。

これにより、動的ポリモーフィズムが実現され、基底クラスのポインタや参照を通じて派生クラスの関数を呼び出すことができます。

以下は、基底クラスでのvirtual関数の定義の例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() { // virtual関数の定義
        cout << "Baseクラスのshow関数" << endl;
    }
};
int main() {
    Base b;
    b.show(); // Baseクラスのshow関数が呼ばれる
    return 0;
}
Baseクラスのshow関数

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

派生クラスでは、基底クラスで定義されたvirtual関数をオーバーライドすることができます。

オーバーライドされた関数は、基底クラスのポインタや参照を通じて呼び出すことができ、実行時にどの関数が呼ばれるかが決まります。

以下は、派生クラスでのオーバーライドの例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() { // virtual関数
        cout << "Baseクラスのshow関数" << endl;
    }
};
class Derived : public Base {
public:
    void show() override { // オーバーライド
        cout << "Derivedクラスのshow関数" << endl;
    }
};
int main() {
    Base* b = new Derived(); // 基底クラスのポインタ
    b->show(); // Derivedクラスのshow関数が呼ばれる
    delete b; // メモリの解放
    return 0;
}
Derivedクラスのshow関数

基底クラスのポインタや参照を使った呼び出し

基底クラスのポインタや参照を使用することで、派生クラスのオーバーライドされた関数を呼び出すことができます。

これにより、同じインターフェースを持つ異なるクラスのオブジェクトを扱うことが可能になります。

以下はその例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() {
        cout << "Baseクラスのshow関数" << endl;
    }
};
class Derived : public Base {
public:
    void show() override {
        cout << "Derivedクラスのshow関数" << endl;
    }
};
void display(Base* b) { // 基底クラスのポインタを引数に取る関数
    b->show(); // オーバーライドされた関数が呼ばれる
}
int main() {
    Derived d;
    display(&d); // Derivedクラスのshow関数が呼ばれる
    return 0;
}
Derivedクラスのshow関数

コンストラクタやデストラクタにおけるvirtualの扱い

C++では、コンストラクタにvirtualを付けることはできませんが、デストラクタにはvirtualを付けることが推奨されます。

これにより、基底クラスのポインタを通じて派生クラスのオブジェクトが削除される際に、正しいデストラクタが呼ばれることが保証されます。

以下はデストラクタにおけるvirtualの例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual ~Base() { // virtualデストラクタ
        cout << "Baseクラスのデストラクタ" << endl;
    }
};
class Derived : public Base {
public:
    ~Derived() override { // オーバーライド
        cout << "Derivedクラスのデストラクタ" << endl;
    }
};
int main() {
    Base* b = new Derived(); // 基底クラスのポインタ
    delete b; // Derivedクラスのデストラクタが呼ばれる
    return 0;
}
Derivedクラスのデストラクタ
Baseクラスのデストラクタ

純粋仮想関数と抽象クラス

純粋仮想関数は、基底クラスで= 0を使って定義される関数であり、これによりそのクラスは抽象クラスとなります。

抽象クラスはインスタンス化できず、派生クラスで必ずオーバーライドする必要があります。

以下は純粋仮想関数と抽象クラスの例です。

#include <iostream>
using namespace std;
class AbstractBase {
public:
    virtual void show() = 0; // 純粋仮想関数
};
class ConcreteDerived : public AbstractBase {
public:
    void show() override { // オーバーライド
        cout << "ConcreteDerivedクラスのshow関数" << endl;
    }
};
int main() {
    ConcreteDerived d;
    d.show(); // ConcreteDerivedクラスのshow関数が呼ばれる
    return 0;
}
ConcreteDerivedクラスのshow関数

virtual修飾子の実装例

基本的なvirtual関数の例

virtual修飾子を使った基本的な関数の定義は、基底クラスでの関数の動的バインディングを示します。

以下は、virtual関数を持つ基底クラスの例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() { // virtual関数
        cout << "Baseクラスのshow関数" << endl;
    }
};
int main() {
    Base b;
    b.show(); // Baseクラスのshow関数が呼ばれる
    return 0;
}
Baseクラスのshow関数

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

派生クラスで基底クラスのvirtual関数をオーバーライドすることで、異なる動作を実現できます。

以下は、オーバーライドの例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() { // virtual関数
        cout << "Baseクラスのshow関数" << endl;
    }
};
class Derived : public Base {
public:
    void show() override { // オーバーライド
        cout << "Derivedクラスのshow関数" << endl;
    }
};
int main() {
    Derived d;
    d.show(); // Derivedクラスのshow関数が呼ばれる
    return 0;
}
Derivedクラスのshow関数

基底クラスのポインタを使った動的ポリモーフィズムの例

基底クラスのポインタを使用することで、動的ポリモーフィズムを実現できます。

以下はその例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() { // virtual関数
        cout << "Baseクラスのshow関数" << endl;
    }
};
class Derived : public Base {
public:
    void show() override { // オーバーライド
        cout << "Derivedクラスのshow関数" << endl;
    }
};
int main() {
    Base* b = new Derived(); // 基底クラスのポインタ
    b->show(); // Derivedクラスのshow関数が呼ばれる
    delete b; // メモリの解放
    return 0;
}
Derivedクラスのshow関数

純粋仮想関数を使った抽象クラスの例

純粋仮想関数を使用することで、抽象クラスを定義し、派生クラスで必ずオーバーライドさせることができます。

以下はその例です。

#include <iostream>
using namespace std;
class AbstractBase {
public:
    virtual void show() = 0; // 純粋仮想関数
};
class ConcreteDerived : public AbstractBase {
public:
    void show() override { // オーバーライド
        cout << "ConcreteDerivedクラスのshow関数" << endl;
    }
};
int main() {
    ConcreteDerived d;
    d.show(); // ConcreteDerivedクラスのshow関数が呼ばれる
    return 0;
}
ConcreteDerivedクラスのshow関数

virtual修飾子の注意点

パフォーマンスへの影響

virtual関数を使用すると、関数呼び出しの際に動的バインディングが行われるため、通常の関数呼び出しよりも若干のオーバーヘッドが発生します。

これは、呼び出し時に仮想テーブル(vtable)を参照する必要があるためです。

ただし、現代のコンパイラは最適化を行うため、パフォーマンスへの影響は通常は小さいです。

特に、頻繁に呼び出される関数には注意が必要です。

オーバーライドのミスを防ぐためのoverrideキーワード

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

これにより、関数のシグネチャが一致しない場合にコンパイルエラーが発生し、オーバーライドのミスを防ぐことができます。

以下はその例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() { // virtual関数
        cout << "Baseクラスのshow関数" << endl;
    }
};
class Derived : public Base {
public:
    void show() override { // overrideキーワード
        cout << "Derivedクラスのshow関数" << endl;
    }
};

finalキーワードとの併用

finalキーワードを使用することで、特定のクラスや関数がさらに派生されないことを明示できます。

これにより、意図しないオーバーライドを防ぐことができます。

以下はその例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() { // virtual関数
        cout << "Baseクラスのshow関数" << endl;
    }
};
class Derived final : public Base { // finalキーワード
public:
    void show() override {
        cout << "Derivedクラスのshow関数" << endl;
    }
};

デストラクタにvirtualを付けるべき理由

基底クラスのデストラクタにvirtualを付けることで、基底クラスのポインタを通じて派生クラスのオブジェクトが削除される際に、正しいデストラクタが呼ばれることが保証されます。

これを怠ると、リソースリークや未定義の動作を引き起こす可能性があります。

以下はその例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual ~Base() { // virtualデストラクタ
        cout << "Baseクラスのデストラクタ" << endl;
    }
};
class Derived : public Base {
public:
    ~Derived() override { // オーバーライド
        cout << "Derivedクラスのデストラクタ" << endl;
    }
};

コピーコンストラクタや代入演算子におけるvirtualの扱い

C++では、コピーコンストラクタや代入演算子にvirtualを付けることはできません。

これらは通常、オブジェクトのコピーや代入を行うための特別なメンバー関数であり、ポリモーフィズムの対象にはなりません。

したがって、基底クラスのポインタを使って派生クラスのオブジェクトをコピーする場合は、注意が必要です。

以下はその例です。

#include <iostream>
using namespace std;
class Base {
public:
    virtual void show() {
        cout << "Baseクラスのshow関数" << endl;
    }
    // コピーコンストラクタはvirtualではない
    Base(const Base&) {
        cout << "Baseクラスのコピーコンストラクタ" << endl;
    }
};
class Derived : public Base {
public:
    void show() override {
        cout << "Derivedクラスのshow関数" << endl;
    }
};
int main() {
    Derived d;
    Base b = d; // 基底クラスのコピーコンストラクタが呼ばれる
    b.show(); // Baseクラスのshow関数が呼ばれる
    return 0;
}
Baseクラスのコピーコンストラクタ
Baseクラスのshow関数

このように、コピーコンストラクタや代入演算子の扱いには注意が必要です。

応用例

インターフェースとしての抽象クラスの利用

抽象クラスは、インターフェースを定義するために使用されます。

これにより、異なるクラスが同じメソッドを実装することを強制し、統一されたインターフェースを提供します。

以下は、抽象クラスをインターフェースとして利用する例です。

#include <iostream>
using namespace std;
class IShape { // インターフェースとしての抽象クラス
public:
    virtual void draw() = 0; // 純粋仮想関数
};
class Circle : public IShape {
public:
    void draw() override { // オーバーライド
        cout << "円を描画" << endl;
    }
};
class Square : public IShape {
public:
    void draw() override { // オーバーライド
        cout << "四角を描画" << endl;
    }
};
int main() {
    IShape* shape1 = new Circle();
    IShape* shape2 = new Square();
    
    shape1->draw(); // 円を描画
    shape2->draw(); // 四角を描画
    
    delete shape1;
    delete shape2;
    return 0;
}
円を描画
四角を描画

プラグインシステムでのvirtual関数の活用

プラグインシステムでは、基底クラスを定義し、異なるプラグインがその基底クラスを継承して機能を追加します。

これにより、動的に機能を拡張することが可能になります。

以下はその例です。

#include <iostream>
using namespace std;
class Plugin { // 基底クラス
public:
    virtual void execute() = 0; // 純粋仮想関数
};
class PluginA : public Plugin {
public:
    void execute() override { // オーバーライド
        cout << "PluginAの実行" << endl;
    }
};
class PluginB : public Plugin {
public:
    void execute() override { // オーバーライド
        cout << "PluginBの実行" << endl;
    }
};
int main() {
    Plugin* plugins[] = { new PluginA(), new PluginB() };
    
    for (Plugin* plugin : plugins) {
        plugin->execute(); // 各プラグインの実行
    }
    
    for (Plugin* plugin : plugins) {
        delete plugin; // メモリの解放
    }
    return 0;
}
PluginAの実行
PluginBの実行

テストコードにおけるモッククラスの作成

テストコードでは、依存関係を持つクラスの動作を模倣するためにモッククラスを作成します。

これにより、特定の条件下での動作をテストすることができます。

以下はモッククラスの例です。

#include <iostream>
using namespace std;
class IService { // インターフェース
public:
    virtual void performAction() = 0; // 純粋仮想関数
};
class MockService : public IService { // モッククラス
public:
    void performAction() override { // オーバーライド
        cout << "モックサービスのアクション" << endl;
    }
};
void clientFunction(IService* service) {
    service->performAction(); // サービスのアクションを実行
}
int main() {
    MockService mockService;
    clientFunction(&mockService); // モックサービスを使用
    return 0;
}
モックサービスのアクション

複数の派生クラスを持つ場合の設計パターン

複数の派生クラスを持つ場合、戦略パターンやファクトリーパターンなどの設計パターンを使用することで、柔軟な設計が可能になります。

以下はファクトリーパターンの例です。

#include <iostream>
using namespace std;
class Shape { // 基底クラス
public:
    virtual void draw() = 0; // 純粋仮想関数
};
class Circle : public Shape {
public:
    void draw() override { // オーバーライド
        cout << "円を描画" << endl;
    }
};
class Square : public Shape {
public:
    void draw() override { // オーバーライド
        cout << "四角を描画" << endl;
    }
};
class ShapeFactory { // ファクトリークラス
public:
    static Shape* createShape(const string& type) {
        if (type == "circle") {
            return new Circle();
        } else if (type == "square") {
            return new Square();
        }
        return nullptr;
    }
};
int main() {
    Shape* shape1 = ShapeFactory::createShape("circle");
    Shape* shape2 = ShapeFactory::createShape("square");
    
    shape1->draw(); // 円を描画
    shape2->draw(); // 四角を描画
    
    delete shape1;
    delete shape2;
    return 0;
}
円を描画
四角を描画

マルチスレッド環境でのvirtual関数の注意点

マルチスレッド環境では、virtual関数を使用する際にスレッドセーフであることを考慮する必要があります。

特に、共有リソースにアクセスする場合は、適切なロック機構を使用してデータ競合を防ぐ必要があります。

以下はその注意点を示す例です。

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
class Base {
public:
    virtual void show() { // virtual関数
        cout << "Baseクラスのshow関数" << endl;
    }
};
class Derived : public Base {
public:
    void show() override { // オーバーライド
        cout << "Derivedクラスのshow関数" << endl;
    }
};
mutex mtx; // ミューテックス
void threadFunction(Base* b) {
    lock_guard<mutex> lock(mtx); // ロックを取得
    b->show(); // 関数の呼び出し
}
int main() {
    Derived d;
    thread t1(threadFunction, &d);
    thread t2(threadFunction, &d);
    
    t1.join(); // スレッド1の終了を待つ
    t2.join(); // スレッド2の終了を待つ
    return 0;
}
Derivedクラスのshow関数
Derivedクラスのshow関数

このように、マルチスレッド環境でのvirtual関数の使用には、適切なロック機構を用いることが重要です。

よくある質問

virtual関数を使わないとどうなる?

virtual関数を使用しない場合、基底クラスのポインタや参照を通じて派生クラスのオーバーライドされた関数を呼び出すことができません。

これにより、動的ポリモーフィズムが失われ、基底クラスの関数が常に呼ばれることになります。

結果として、異なるクラスのオブジェクトを同じインターフェースで扱うことができず、柔軟性が低下します。

例えば、基底クラスのポインタを使って派生クラスのメソッドを呼び出すことができず、意図した動作が実現できなくなります。

なぜデストラクタにvirtualを付ける必要があるのか?

基底クラスのデストラクタにvirtualを付けることで、基底クラスのポインタを通じて派生クラスのオブジェクトが削除される際に、正しいデストラクタが呼ばれることが保証されます。

これを怠ると、派生クラスのリソースが適切に解放されず、メモリリークや未定義の動作を引き起こす可能性があります。

特に、リソース管理が重要なクラス階層においては、デストラクタにvirtualを付けることが非常に重要です。

virtual関数はパフォーマンスにどの程度影響するのか?

virtual関数を使用すると、通常の関数呼び出しよりも若干のオーバーヘッドが発生します。

これは、呼び出し時に仮想テーブル(vtable)を参照する必要があるためです。

ただし、現代のコンパイラやプロセッサはこのオーバーヘッドを最小限に抑えるための最適化を行っているため、実際の影響は通常は小さいです。

特に、頻繁に呼び出される関数やパフォーマンスが重要な部分では、virtual関数の使用を慎重に検討する必要がありますが、一般的なアプリケーションでは大きな問題にはならないことが多いです。

まとめ

この記事では、C++におけるvirtual修飾子の使い方やその重要性について詳しく解説しました。

基底クラスでのvirtual関数の定義から、派生クラスでのオーバーライド、デストラクタにおける注意点、さらには抽象クラスやプラグインシステムでの応用例まで、多岐にわたる内容を取り上げました。

これを機に、実際のプログラミングにおいてvirtual関数を効果的に活用し、柔軟で拡張性のあるコードを書くことを目指してみてください。

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