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

はじめに

C++の抽象クラス(abstract class)とは、共通のインターフェースだけを定義して、実際の処理は子クラスに任せるためのクラスです。オブジェクト指向における「型の共通化」や「設計の基盤」に使われます。

1. 抽象クラスとは

抽象クラスは「インスタンスを生成できないクラス」です。少なくとも1つ以上の純粋仮想関数(pure virtual function)を含みます。

関数に = 0 を付けると、その関数は純粋仮想関数になります(戻り値が0という意味ではありません)。

// 20_abstract_base.cpp
#include <iostream>
using namespace std;

class Enemy {
public:
    // 純粋仮想関数:派生クラスで必ず実装する必要がある
    virtual void attack() = 0;
};

class Slime : public Enemy {
public:
    void attack() override {
        cout << "スライムの体当たり!" << endl;
    }
};

class Goblin : public Enemy {
public:
    void attack() override {
        cout << "ゴブリンの斬撃!" << endl;
    }
};

int main() {
    // Enemy e; // エラー:抽象クラスはインスタンス生成できない

    Enemy* e1 = new Slime();
    Enemy* e2 = new Goblin();

    e1->attack();
    e2->attack();

    delete e1;
    delete e2;
}

実行結果(例)

スライムの体当たり!
ゴブリンの斬撃!
virtual を付けただけの関数なら、派生クラスで定義しなくても構いません。ただし、純粋仮想関数(= 0 を付けたもの)の場合は、派生クラスで定義しないとコンパイルエラーになります。

2. 純粋仮想関数とは

純粋仮想関数は「中身を持たない関数の宣言だけ」です。これにより、派生クラスに「この関数を必ず定義しなければならない」というルールを課すことができます。

virtual void attack() = 0; // 実装を持たない、命令だけの関数
抽象クラスは「共通の形を定めるためのクラス」です。

3. 抽象クラスを使うメリット

メリット内容
統一されたインターフェース共通の基底クラスで扱えるため、コードが整理される
拡張性新しい敵キャラを追加しても、共通のポインタで操作可能
安全性関数の実装忘れをコンパイル時に検出できる

4. ポリモーフィズムとの関係

抽象クラスと純粋仮想関数を使うと、異なる型のオブジェクトを同じインターフェースで扱うことができます。これは「ポリモーフィズム(多態性)」の代表的な例です。

// 20_polymorphism.cpp
#include <iostream>
#include <vector>
using namespace std;

class Enemy {
public:
    virtual void attack() = 0; // 純粋仮想関数
    virtual ~Enemy() {}        // 仮想デストラクタ
};

class Slime : public Enemy {
public:
    void attack() override { cout << "スライムの体当たり!" << endl; }
};

class Goblin : public Enemy {
public:
    void attack() override { cout << "ゴブリンの斬撃!" << endl; }
};

int main() {
    vector<Enemy*> enemies;
    enemies.push_back(new Slime());
    enemies.push_back(new Goblin());

    for (Enemy* e : enemies) {
        e->attack(); // 型ごとに異なるattack()が呼ばれる(多態性)
    }

    for (Enemy* e : enemies) delete e;
}

実行結果(例)

スライムの体当たり!
ゴブリンの斬撃!
抽象クラスを基底にすることで、共通のポインタ(Enemy*)で複数の型を一括管理できます。これはポリモーフィズム(多態性)の応用例でもあります。

コーディング演習

演習1:基本コードを動かす

20_01_abstract.cpp を作成し、次のコードを入力して実行しましょう。Enemy を抽象クラスとして定義し、派生クラスで attack() を実装します。
// 20_abstract_base.cpp
#include <iostream>
#include <vector>
using namespace std;

class Enemy {
public:
    virtual void attack() = 0; // 純粋仮想関数
    virtual ~Enemy() {}
};

class Slime : public Enemy {
public:
    void attack() override {
        cout << "スライムの体当たり!" << endl;
    }
};

class Goblin : public Enemy {
public:
    void attack() override {
        cout << "ゴブリンの斬撃!" << endl;
    }
};

int main() {
    vector<Enemy*> enemies;
    enemies.push_back(new Slime());
    enemies.push_back(new Goblin());

    for (Enemy* e : enemies) {
        e->attack();
    }
    for (Enemy* e : enemies) delete e;
    return 0;
}

演習2:コードを改造する

20_01_Abstract20_abstract_base.cpp をコピーして 20_02_AbstractMod に貼り付け、次の変更を加えてみましょう:

まとめ

理解度チェック

問題20-1

次のコードのうち、コンパイルエラーになるものはどれか。

class Shape {
public:
    virtual void draw() = 0;
};
class Circle : public Shape {
public:
    void draw() override { /* 描画処理 */ }
};
解説を表示 正解:イ
Shape は純粋仮想関数を持つ抽象クラスなので、直接インスタンス化(Shape s;)はコンパイルエラーになる。ポインタ型(Shape*)での宣言は問題ない。

問題20-2

純粋仮想関数の宣言として正しいものはどれか。

解説を表示 正解:ウ
純粋仮想関数は virtual キーワードと = 0 の両方が必要。virtual なしでは機能しない。C++に abstract キーワードはない。

問題20-3

抽象クラスを使う最大のメリットとして最も適切なものはどれか。

解説を表示 正解:ア
抽象クラスの最大のメリットは「設計の強制力」。純粋仮想関数を実装しない派生クラスはインスタンス化できず、コンパイル時にエラーとなる。実装忘れを防ぎ、複数人での開発でも設計を統一できる。

問題20-4

次のコードを実行したとき、出力はどれか。

#include <iostream>
#include <vector>
using namespace std;
class Enemy {
public:
    virtual void attack() = 0;
    virtual ~Enemy() {}
};
class Slime : public Enemy {
public:
    void attack() override { cout << "A" << endl; }
};
class Goblin : public Enemy {
public:
    void attack() override { cout << "B" << endl; }
};
int main() {
    vector<Enemy*> v = { new Slime(), new Goblin(), new Slime() };
    for (Enemy* e : v) { e->attack(); delete e; }
}
解説を表示 正解:イ
vector の要素順に Slime, Goblin, Slime が格納されているので、ABA が各改行付きで出力される。純粋仮想関数によるポリモーフィズムの動作。