C++ Learning

第43回 仮想関数と override/final

派生クラスで振る舞いを差し替え、基底型のポインタや参照から派生の実装が呼ばれる仕組み、それが仮想関数 (virtual function)。動的多態 (dynamic polymorphism) の核心です。override で上書き意図を明示、final で上書き禁止、仮想デストラクタでポインタ経由の破棄を安全に。

このページで押さえること
✅ 最低限ここだけ覚える
  • virtual を基底に付けた関数は上書き可能
  • 派生で上書き時は override を付ける(タイポ防止)
  • 基底ポインタから呼ぶと実体の実装が呼ばれる
  • 仮想関数を持つクラスは仮想デストラクタが必須
⭐ 余裕があれば読む
  • final でそれ以上の上書きを禁止
  • 仮想関数のコスト(vtable 間接呼び出し)
  • 共変戻り値
  • 基底クラスを呼ぶ Base::f()

1. まず触ってみる ― speak() の多態

基底 Animalspeak() を持ち、派生 Dog/Cat がそれぞれ上書き。基底のポインタから呼ぶだけで、実体に応じた関数が呼ばれるのが多態の醍醐味。

▶ 派生の speak を基底ポインタから呼ぶ
Animal* p = ; p->speak();

最小コード

first_virtual.cpp最小例
class Animal { public: virtual void speak() { std::cout << "(音)\n"; } virtual ~Animal() = default; // 仮想デストラクタ必須 }; class Dog : public Animal { public: void speak() override { std::cout << "ワン!\n"; } }; class Cat : public Animal { public: void speak() override { std::cout << "ニャー\n"; } }; std::vector<std::unique_ptr<Animal>> animals; animals.push_back(std::make_unique<Dog>()); animals.push_back(std::make_unique<Cat>()); for (auto& a : animals) a->speak(); // 出力: // ワン! // ニャー
ここまでで覚えること(3 つ):
  • 基底の関数に virtual を付ける
  • 派生で上書きするとき override を付ける
  • 基底に仮想デストラクタvirtual ~Base() = default;)を必ず書く

よくある素朴な疑問

Q. virtual を付けないとどうなる?
→ 基底ポインタから呼ぶと基底の関数が呼ばれます。派生の実装が呼ばれない。多態にならない(次章 vtable の仕組みで解説)。

Q. override は必須?
→ 動作上は必須ではないが強く推奨。タイポで別関数を定義してしまう事故を防げる(コンパイル時に「basename の virtual がない」と教えてくれる)。

2. virtual と override

override を付ける推奨
class Base { public: virtual void greet() const; }; class Derived : public Base { public: void greet() const override; // 基底にvirtualが // なければエラーに };
override 無しの罠気づきにくい
class Base { public: virtual void greet() const; }; class Derived : public Base { public: void greet(); // const 付け忘れ! // 「新しい関数」として追加される // 基底の greet() は上書きされない };

override を付けていれば、右側のような「うっかり別関数」はコンパイルエラーになります。必ず付ける習慣を。

virtual は基底だけに書く

virtual キーワードは基底クラスの宣言に書きます。派生で再度 virtual を書く必要はありません(書いても動きます)。派生には override だけを付けます。

基底の実装を呼ぶ

Base::f() で明示呼び出し拡張
class Dog : public Animal { public: void speak() override { Animal::speak(); // 基底の処理を先に std::cout << "ワン!\n"; } };

派生の中から基底の実装を呼び出して拡張するパターン。基底の処理を完全に置き換えるのではなく、追加したい場合に使います。

3. 仮想デストラクタ(必須)

基底ポインタから派生オブジェクトを delete する場面では、基底のデストラクタが virtual でないと派生の dtor が呼ばれないという重大な問題があります。

NGリーク
class Base { ~Base() { } // virtual 無し }; class Derived : public Base { int* data_; public: Derived() : data_(new int[100]) {} ~Derived() { delete[] data_; } }; Base* p = new Derived(); delete p; // ← Derived::~Derived() が呼ばれない! // data_ がリーク(未定義動作)
OKvirtual dtor
class Base { virtual ~Base() = default; // 必ず virtual }; class Derived : public Base { int* data_; public: Derived() : data_(new int[100]) {} ~Derived() override { delete[] data_; } }; Base* p = new Derived(); delete p; // Derived → Base の順で dtor が呼ばれる
鉄則:「多態のために基底クラスとして使う型」には、必ず virtual ~Base() = default; を書く。仮想関数を 1 つでも持つなら必須。

= default の利点

virtual ~Base() = default; と書くと、コンパイラ生成版をそのまま使いつつ、virtual だけ明示できます。空の {} を書くよりも意図が明確。

4. final で上書きを閉じる

final は「これ以上上書きしない/継承しない」宣言。設計意図を明示し、最適化にも寄与します。

関数に final上書き禁止
class Middle : public Base { public: void f() override final; // これ以上上書き禁止 }; class Bottom : public Middle { public: void f() override; // エラー: final に上書きできない };
クラスに final継承禁止
class Leaf final : public Base { // Leaf を継承しようとするとエラー }; class Bad : public Leaf { }; // エラー
使いどころ:
  • クラス階層の末端であることを明示したい(設計の意図)
  • 最適化(コンパイラが仮想呼び出しを直接呼び出しに置き換えられる)
ここまでで仮想関数の使い方は OK
最後に「なぜ必要か」を非仮想との違いで再確認。

5. 非仮想との違い

仮想関数を使わずに基底ポインタから関数を呼ぶと静的バインディング(コンパイル時に呼び出し先が決まる)が行われ、派生の実装は呼ばれません

非仮想静的
class Base { public: void f() { std::cout << "Base\n"; } // virtual なし }; class Derived : public Base { public: void f() { std::cout << "Derived\n"; } }; Base* p = new Derived(); p->f(); // "Base" ← Base の関数が呼ばれる
仮想動的
class Base { public: virtual void f() { std::cout << "Base\n"; } virtual ~Base() = default; }; class Derived : public Base { public: void f() override { std::cout << "Derived\n"; } }; Base* p = new Derived(); p->f(); // "Derived" ← 実体の関数が呼ばれる

コスト

仮想関数はvtable 経由の間接呼び出しなので、直接呼び出しよりわずかに遅い(分岐予測ミス/キャッシュミスの可能性)。ただし現代の CPU ではほとんど無視できるレベル。性能クリティカルでない限り気にしないでください。

仕組みの詳細は次章で。

広告スペース

確認クイズ

virtual と override を 4 問で確認。

Q1. 基底に virtual なしの関数 f()、派生でも f() を定義。Base* p = new Derived(); p->f(); で呼ばれるのは?

Derived::f()
Base::f()
コンパイルエラー
実行時エラー
virtual を付けないと静的バインディング。ポインタの型(Base)で呼び出し先が決まり、Base::f() が呼ばれます。Derived::f() を呼びたければ基底で virtual を付ける必要があります。

Q2. 派生関数に override を付ける最大のメリットは?

実行速度が上がる
タイポ/シグネチャ違いをコンパイル時に検出できる
必須のキーワード
派生クラスが final になる
override はコンパイラに「この関数は基底の virtual の上書きである」と伝え、基底に該当する関数がなければエラーにしてくれます。const 付け忘れ等の事故を防ぐ。

Q3. 仮想関数を持つクラスのデストラクタを virtual にしないと何が起きる?

特に問題ない
仮想関数も動かなくなる
基底ポインタから delete すると派生の dtor が呼ばれず未定義動作
コンパイルエラー
基底ポインタ経由で delete した場合、virtual dtor でないと派生の dtor が呼ばれず、派生部分の後始末がされません。継承される可能性のあるクラスには必ず virtual ~Base() = default;

Q4. final の意味は?

関数が常に最後に呼ばれる
デストラクタの意味
「これ以上上書き/継承できない」という制限
派生クラスを強制する
関数に付ければそれ以上の override を禁止、クラスに付ければそれ以上の継承を禁止。設計意図の明示と、コンパイラの最適化ヒントになります。