C++を学ぶ上で「純粋仮想関数」は避けて通れない重要な概念です。
抽象クラス・多態性・継承と密接に関連するこの仕組みを正確に理解することが、オブジェクト指向設計の深い理解につながります。
本記事では、純粋仮想関数の意味・使い方・抽象クラスとの関係・オーバーライドの必須性・多態性への貢献をわかりやすく解説していきます。
C++を学習中の方・オブジェクト指向の理解を深めたい方・仮想関数と純粋仮想関数の違いに悩む方にぜひ参考にしていただける内容です。
純粋仮想関数の仕組みを理解することで、インターフェースとして機能する設計パターンが実践できるようになります。
抽象化とポリモーフィズムを活用した柔軟で拡張性の高いC++コードを書くための基礎として、本記事の内容を役立てていただければ幸いです。
純粋仮想関数とは何か?仮想関数との違いと基本的な意味
それではまず、純粋仮想関数の基本的な意味と仮想関数との違いについて解説していきます。
C++における純粋仮想関数(Pure Virtual Function)とは、基底クラスで宣言されるが実装を持たず、派生クラスでのオーバーライドが必須である仮想関数です。
宣言の末尾に「= 0」を付けることで純粋仮想関数として定義します。
【仮想関数と純粋仮想関数の比較】
// 通常の仮想関数(実装あり・オーバーライドは任意)
class Animal {
public:
virtual void speak() {
std::cout << "..." << std::endl; // デフォルト実装あり
}
};
// 純粋仮想関数(実装なし・オーバーライド必須)
class Shape {
public:
virtual double area() = 0; // 「= 0」で純粋仮想関数を宣言
virtual double perimeter() = 0;
};
純粋仮想関数を一つでも持つクラスは「抽象クラス(Abstract Class)」となります。
抽象クラスはそれ自体をインスタンス化することはできず、すべての純粋仮想関数をオーバーライドした派生クラスのみがインスタンス化できます。
これはJavaやC#の「abstract」キーワードで修飾されたクラス・メソッドと同等の概念です。
抽象クラスと純粋仮想関数の実装例:Shapeクラスの設計
続いては、抽象クラスと純粋仮想関数の具体的な実装例について確認していきます。
実際のコード例を通じて純粋仮想関数の動作を理解することが、概念の定着に最も効果的です。
【純粋仮想関数を使った抽象クラスの実装例】
// 抽象基底クラス(純粋仮想関数を持つ)
class Shape {
public:
virtual double area() = 0; // 純粋仮想関数
virtual double perimeter() = 0; // 純粋仮想関数
virtual void draw() = 0; // 純粋仮想関数
virtual ~Shape() {} // 仮想デストラクタ
};
// 派生クラス①(すべての純粋仮想関数をオーバーライド)
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() override {
return 3.14159 * radius * radius;
}
double perimeter() override {
return 2 * 3.14159 * radius;
}
void draw() override {
std::cout << "円を描画します" << std::endl;
}
};
// Shape* shape = new Shape(); // エラー!抽象クラスはインスタンス化不可
Shape* shape = new Circle(5.0); // OK:派生クラスはインスタンス化可能
std::cout << shape->area() << std::endl; // Circle::area()が呼ばれる
このコードでShapeクラスはインターフェースとしての役割を果たしており、「すべての形状はarea()・perimeter()・draw()を持つ」というコントラクト(契約)を定義しています。
C++には「interface」キーワードが存在しないため、すべてのメンバーが純粋仮想関数の抽象クラスがインターフェースの役割を担います。
この設計により、新しい形状クラス(Rectangle・Triangle等)を追加する際も既存のコードを変更せずに拡張できます。
純粋仮想関数と多態性(ポリモーフィズム)の関係
続いては、純粋仮想関数と多態性(ポリモーフィズム)の関係について確認していきます。
純粋仮想関数は「多態性(ポリモーフィズム)」を実現するための重要な仕組みです。
多態性とは「同じインターフェースで異なる実装を扱える」という性質であり、オブジェクト指向プログラミングの三大原則の一つです。
【多態性を活用した例】
// 異なる形状を同じShapeポインタで扱える
std::vector<Shape*> shapes;
shapes.push_back(new Circle(5.0));
shapes.push_back(new Rectangle(4.0, 6.0));
shapes.push_back(new Triangle(3.0, 4.0, 5.0));
// すべての形状に対して同じインターフェースで操作できる
for (Shape* s : shapes) {
std::cout << "面積: " << s->area() << std::endl;
s->draw();
}
このコードでは異なる型(Circle・Rectangle・Triangle)のオブジェクトを同じShape*ポインタで管理し、同じインターフェース(area()・draw())で操作しています。
純粋仮想関数によって「すべての派生クラスが必ずこのメソッドを実装している」ことが保証されるため、このような多態的な操作が安全に行えます。
この仕組みはDIPの「高レベルのモジュールが低レベルの具体的な実装に依存しない」という設計原則にも対応しており、変更に強い設計の基盤となります。
純粋仮想関数を使う際の注意点と実践的なポイント
続いては、純粋仮想関数を使う際の注意点と実践的なポイントについて確認していきます。
| 注意点 | 内容 |
|---|---|
| 仮想デストラクタの必須化 | 基底クラスのデストラクタをvirtualにしないとメモリリークが発生する |
| すべての純粋仮想関数のオーバーライド | 一つでもオーバーライドしない派生クラスも抽象クラスになる |
| 純粋仮想関数にも実装を持たせることが可能 | = 0でありながら実装を定義でき、派生クラスから明示的に呼べる |
| 多重継承時のダイヤモンド問題 | 複数の抽象クラスを継承する際はvirtual継承で対処する |
基底クラスのデストラクタを仮想にしない場合、基底クラスのポインタを通じてdelete操作を行うと派生クラスのデストラクタが呼ばれないメモリリークが発生します。
純粋仮想関数を使って抽象クラスを設計する際は、必ず「virtual ~ClassName() {}」の形で仮想デストラクタを定義することが実践上の重要なルールです。
まとめ
純粋仮想関数は基底クラスで「= 0」と宣言され実装を持たない仮想関数であり、派生クラスでのオーバーライドが必須です。
純粋仮想関数を持つクラスは抽象クラスとなり、直接インスタンス化できません。
C++でインターフェースを実現する主要な手段として機能し、多態性(ポリモーフィズム)を安全に実現するための基盤となります。
仮想デストラクタの定義・すべての純粋仮想関数のオーバーライドが実装上の重要な注意点です。
純粋仮想関数を活用した抽象クラス設計が、変更に強く拡張性の高いC++コードを実現する重要な設計手段となるでしょう。