[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関数
の使用には、適切なロック機構を用いることが重要です。
よくある質問
まとめ
この記事では、C++におけるvirtual修飾子
の使い方やその重要性について詳しく解説しました。
基底クラスでのvirtual関数
の定義から、派生クラスでのオーバーライド、デストラクタにおける注意点、さらには抽象クラスやプラグインシステムでの応用例まで、多岐にわたる内容を取り上げました。
これを機に、実際のプログラミングにおいてvirtual関数
を効果的に活用し、柔軟で拡張性のあるコードを書くことを目指してみてください。