C++ Learning

第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. インターフェースとしての抽象クラス

他言語の interfacetrait に相当する使い方ができます。「このクラスに対してできることの契約」を宣言する。

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 が必要。