第45回 純粋仮想関数と抽象クラス
基底クラスで「実装は派生で必ず書け」と要求する仕組みが純粋仮想関数(= 0;)。これを 1 つでも持つクラスは抽象クラスとなり、単独でインスタンス化できなくなります。ちょうど他言語の「インターフェース」のような使い方ができます。
このページで押さえること
✅ 最低限ここだけ覚える
virtual void f() = 0; で純粋仮想関数
- 1 つでも持つクラスは抽象クラス
- 抽象クラスはインスタンス化できない
- 派生で実装を書かないと、その派生も抽象のまま
⭐ 余裕があれば読む
- インターフェース設計のパターン
- ポインタ・参照経由の抽象基底
- 純粋仮想関数でも実装を持たせる
- 抽象デストラクタ
1. 基本 ― 純粋仮想関数
抽象クラスの定義= 0
class Shape { // 抽象クラス
public:
virtual double area() const = 0; // 純粋仮想関数
virtual double perimeter() const = 0;
virtual ~Shape() = default; // 仮想デストラクタは必須
};
// Shape s; ← コンパイルエラー: 抽象クラスはインスタンス化不可
// Shape* p; ← ポインタなら OK
class Circle : public Shape {
double r_;
public:
explicit Circle(double r) : r_(r) {}
double area() const override { return 3.14159 * r_ * r_; }
double perimeter() const override { return 2 * 3.14159 * r_; }
};
class Square : public Shape {
double s_;
public:
explicit Square(double s) : s_(s) {}
double area() const override { return s_ * s_; }
double perimeter() const override { return 4 * s_; }
};
// 使い方:基底ポインタや参照で派生を扱う
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5));
shapes.push_back(std::make_unique<Square>(3));
for (auto& s : shapes)
std::cout << s->area() << "\n"; // 多態で各実装が呼ばれる
ポイント:
virtual ... = 0; で純粋仮想関数(実装なし、派生に強制)
- 1 つでも純粋仮想関数があれば抽象クラス、インスタンス化不可
- 派生ですべての純粋仮想関数を override すると具象クラスに
- 仮想デストラクタ(
virtual ~Base() = default;)は忘れずに
2. インターフェースとしての抽象クラス
他言語の interface や trait に相当する使い方ができます。「このクラスに対してできることの契約」を宣言する。
Printable インターフェース契約
class Printable {
public:
virtual void print(std::ostream& os) const = 0;
virtual ~Printable() = default;
};
// Printable を実装する型なら何でも渡せる関数
void log(const Printable& p) {
p.print(std::cout);
std::cout << "\n";
}
class User : public Printable {
std::string name_;
public:
explicit User(std::string n) : name_(std::move(n)) {}
void print(std::ostream& os) const override {
os << "User(" << name_ << ")";
}
};
Java 風の「純粋インターフェース」(メンバ変数なし、全関数 = 0)を作るのが典型パターン。モックやテストしやすい設計になります。
3. 細かい話
純粋仮想でも実装を持てる
意外と知られていませんが、純粋仮想関数でも実装を書けます。派生からクラス修飾で呼べる「共通処理」を提供したいときに使う。
実装付き純粋仮想上級
class Base {
public:
virtual void f() = 0; // 純粋仮想だが実装も持てる
virtual ~Base() = default;
};
// クラス外で実装を書く
void Base::f() { std::cout << "Base::f 共通処理\n"; }
class Derived : public Base {
public:
void f() override {
Base::f(); // 基底の共通処理を呼ぶ
std::cout << "Derived 固有\n";
}
};
抽象デストラクタ
クラス内に純粋仮想関数がないけれど「このクラスは絶対に単体でインスタンス化させたくない」ときは、デストラクタを純粋仮想にする手があります。ただし実装は書く必要あり。
抽象デストラクタ稀
class AbstractBase {
public:
virtual ~AbstractBase() = 0; // 純粋仮想 dtor
};
// 実装は必須(派生の dtor から呼ばれるため)
AbstractBase::~AbstractBase() = default;
使いどころ: 「基底として使うが、他に純粋仮想にできる関数がない」珍しい状況。通常は何か普通の関数を純粋仮想にする方が自然。
⭐
ここまでで抽象クラスは OK
次は多重継承(第 41 回)。ただし C++ では多重継承は限定的に使うことが推奨。
確認クイズ
抽象クラスを 4 問で確認。
Q1. 純粋仮想関数の宣言として正しいのは?
virtual void f();
abstract void f();
virtual void f() = 0;
void f() override = 0;
C++ の純粋仮想は = 0 を関数宣言の末尾に付ける特殊な構文。abstract キーワードは存在しません(Java や C# の流儀)。
Q2. 抽象クラス(純粋仮想を含むクラス)に対してできることは?
インスタンス化(変数として作成)
抽象クラスの関数を直接呼ぶ
ポインタや参照型として使う(派生を指す)
何もできない
抽象クラスは直接インスタンス化できません。しかしポインタ・参照の型としては使えて、派生クラスのインスタンスを指すのが普通の使い方です。
Q3. 純粋仮想関数をすべて上書きせずに派生クラスを作るとどうなる?
コンパイルエラー
警告だけで通る
その派生クラスも抽象クラスになり、インスタンス化できない
自動的に空の実装が生成される
抽象クラスを継承しても、純粋仮想関数を全部 override しなければその派生も抽象クラスのまま。インスタンス化は不可で、さらに派生したときに初めて具象化できます。
Q4. 抽象クラスに仮想デストラクタは必要?
不要(インスタンス化できないから)
必要(基底ポインタから派生を delete するときに必要)
必須ではないが推奨
C++20 以降は不要
抽象クラスの派生インスタンスを基底ポインタ経由で delete する場合、基底のデストラクタが virtual でないと派生の dtor が呼ばれず未定義動作に。仮想関数を持つすべてのクラスに仮想 dtor が必要。