C++の抽象クラスとは、そのクラス自体から直接オブジェクトを作るのではなく、派生クラスに共通のルールや機能を定義するためのクラスです。
抽象クラスは、具体的な処理をすべて自分で持つクラスではありません。
むしろ、「この種類のクラスなら、この機能を持っているべき」という共通の約束を作るために使われます。
たとえば、「動物」という大きな分類を考えると、犬や猫は具体的な存在ですが、「動物」という概念そのものは抽象的です。
犬は犬らしく鳴き、猫は猫らしく鳴きます。
しかし、「動物」というだけでは、どのように鳴くのかまでは決まりません。
C++の抽象クラスも同じように、共通の性質や操作だけを定義し、具体的な動作は派生クラス側に任せる仕組みです。
抽象クラスになる条件
C++では、純粋仮想関数を1つ以上持つクラスが抽象クラスになります。
純粋仮想関数とは、基底クラス側で「この関数は必要である」と示しつつ、具体的な処理内容は派生クラスに任せる関数です。
抽象クラスは、具体的な処理が未完成の部分を含むため、そのままオブジェクトを作ることはできません。
派生クラスが必要な処理を実装することで、実際に使えるクラスになります。
純粋仮想関数を持つクラスが抽象クラスになる
抽象クラスかどうかを決める重要な要素は、純粋仮想関数の有無です。
クラスの中に純粋仮想関数が1つでもあれば、そのクラスは抽象クラスになります。
純粋仮想関数は、派生クラスに対して「この関数を実装する必要がある」と求めるための仕組みです。
これにより、複数の派生クラスに共通した機能を持たせることができます。
抽象クラスは未完成のクラスではなく設計上の土台
抽象クラスは、単に不完全なクラスというよりも、設計上の土台となるクラスです。
具体的な処理を派生クラスに任せることで、共通の使い方を保証しながら、実際の動作を柔軟に変えられます。
そのため、抽象クラスは大規模なプログラムや、拡張性を意識した設計でよく使われます。
抽象クラスは直接インスタンス化できない
抽象クラスの大きな特徴は、そのクラス自体のオブジェクトを直接作れないことです。
抽象クラスには、具体的な処理が決まっていない純粋仮想関数が含まれています。
そのため、抽象クラスだけでは、実行時にどのような処理をすればよいか決められません。
抽象クラスそのもののオブジェクトは作れない
抽象クラスは、あくまで派生クラスのための基底クラスです。
具体的なオブジェクトを作るには、抽象クラスを継承した派生クラス側で、必要な純粋仮想関数を実装する必要があります。
たとえば、「図形」という抽象的なクラスがある場合、それ自体は具体的な形を持ちません。
円や長方形のような具体的なクラスがあって初めて、面積の計算方法などを決められます。
ポインタや参照としては使える
抽象クラスは直接オブジェクトを作れませんが、ポインタや参照の型として使うことはできます。
この性質が、C++の抽象クラスを使ううえで非常に重要です。
抽象クラスをポインタや参照として扱うことで、複数の派生クラスを共通の型として利用できます。
たとえば、犬、猫、鳥といった別々のクラスを、すべて「動物」として扱うことができます。
実際の動作はそれぞれ異なっていても、呼び出す側は共通の型を通じて操作できます。
純粋仮想関数とは
純粋仮想関数とは、基底クラス側で「この関数を持つべき」と定義し、具体的な処理は派生クラス側に任せる関数です。
抽象クラスを理解するうえで、純粋仮想関数は中心となる考え方です。
派生クラスに実装を求める関数
純粋仮想関数は、派生クラスに対して特定の関数の実装を求めます。
たとえば、「保存する」「読み込む」「描画する」「面積を求める」「支払う」といった操作は、さまざまなクラスで共通して必要になることがあります。
しかし、具体的な処理内容はクラスごとに異なります。
ファイルに保存する場合、データベースに保存する場合、クラウドに保存する場合では、同じ「保存する」という操作でも中身は違います。
このような場面で、抽象クラスは共通の操作だけを定義し、実際の処理は派生クラスに任せます。
実装しない派生クラスも抽象クラスになる
抽象クラスを継承した派生クラスが、すべての純粋仮想関数を実装しない場合、その派生クラスも抽象クラスになります。
つまり、抽象クラスを継承したからといって、必ずすぐに具体的なクラスになるわけではありません。
派生クラスを実際にオブジェクトとして使いたい場合は、継承した純粋仮想関数をすべて実装する必要があります。
純粋仮想関数にも実装を書ける
C++では、純粋仮想関数にも実装を持たせることができます。
これは少し意外に感じるかもしれません。
純粋仮想関数というと、処理内容をまったく持てない関数のように思われがちですが、C++では必ずしもそうではありません。
ただし、純粋仮想関数に実装があっても、その関数が純粋仮想関数として定義されている限り、そのクラスは抽象クラスのままです。
この仕組みは、派生クラスに実装を求めながら、一部の共通処理を基底クラス側に用意したい場合に使われます。
抽象クラスを使う目的
抽象クラスは、単に継承を使うための仕組みではありません。
主な目的は、共通の使い方を定義し、具体的な処理を派生クラスに任せることです。
共通のインターフェースを定義するため
抽象クラスは、複数のクラスに共通するインターフェースを定義するために使われます。
インターフェースとは、外部から見たときに「どのような操作ができるか」を表すものです。
たとえば、決済処理では、クレジットカード決済、QRコード決済、銀行振込など、具体的な支払い方法は異なります。
しかし、どの方法にも「支払う」という共通の操作があります。
抽象クラスで「支払う」という操作を定義しておけば、呼び出し側は具体的な支払い方法を意識せずに処理できます。
派生クラスに必要な機能を強制するため
抽象クラスを使うと、派生クラスに対して必要な機能を明確にできます。
たとえば、すべての図形クラスに「面積を求める機能」を持たせたい場合、抽象クラス側でその機能を定義しておけば、派生クラス側で実装が求められます。
これにより、クラス設計のばらつきを防ぎやすくなります。
複数人で開発する場合や、後から機能を追加する場合でも、「この種類のクラスにはこの機能が必要」というルールが明確になります。
ポリモーフィズムを実現するため
抽象クラスは、C++のポリモーフィズムと深く関係しています。
ポリモーフィズムとは、異なる種類のオブジェクトを共通の型として扱いながら、実際の動作はそれぞれのクラスに応じて切り替わる仕組みです。
たとえば、犬、猫、鳥をすべて「動物」として扱いながら、鳴く処理を呼び出すと、それぞれ異なる鳴き方をさせることができます。
呼び出し側は具体的なクラスを意識せず、共通の操作だけを使えます。
その一方で、実際の動作は派生クラスごとに変わります。
抽象クラスのメリット
抽象クラスを使うと、コードの共通化や拡張性の向上につながります。
特に、大きなプログラムや変更が多いシステムでは効果を発揮します。
実装のルールを統一できる
抽象クラスを使うと、派生クラスが持つべき機能を統一できます。
たとえば、複数の出力形式に対応するクラスを作る場合、すべてのクラスに「出力する」という共通の機能を求めることができます。
これにより、クラスごとに関数名や使い方がばらばらになることを防げます。
設計の一貫性が高まり、コードを読む人にとっても理解しやすくなります。
呼び出し側のコードを簡潔にできる
抽象クラスを使うと、呼び出し側は具体的な派生クラスを細かく意識しなくて済みます。
たとえば、保存先がファイルなのか、データベースなのか、クラウドなのかを呼び出し側が毎回判断する必要はありません。
共通の操作だけを呼び出せば、実際の保存処理は派生クラス側で実行されます。
このように、抽象クラスは呼び出し側のコードをシンプルにし、保守性を高めます。
機能追加に強い設計にできる
抽象クラスを使うと、新しい派生クラスを追加しやすくなります。
たとえば、既存の決済システムに新しい決済方法を追加する場合でも、抽象クラスで定義された共通のルールに従って新しいクラスを作れば、既存の処理を大きく変更せずに済むことがあります。
このように、抽象クラスは拡張性の高い設計に役立ちます。
テストしやすくなる
抽象クラスを使うと、テスト用の派生クラスを作りやすくなります。
外部サービス、データベース、ネットワーク通信などに依存する処理では、本番環境の機能をそのまま使うとテストが難しくなる場合があります。
そのようなとき、抽象クラスを使って依存先を切り替えられる設計にしておくと、テスト用の仮実装を使いやすくなります。
結果として、安全に動作確認を行いやすくなります。
抽象クラスを使うときの注意点
抽象クラスは便利ですが、使い方を誤ると設計が複雑になったり、予期しない不具合につながったりします。
デストラクタは基本的に仮想デストラクタにする
抽象クラスを基底クラスとして使う場合、デストラクタは基本的に仮想デストラクタにするのが安全です。
特に、基底クラスのポインタを通して派生クラスのオブジェクトを削除する可能性がある場合は重要です。
仮想デストラクタになっていないと、派生クラス側のデストラクタが正しく呼ばれず、リソースの解放が不完全になる可能性があります。
ただし、設計によっては、基底クラス経由で削除できないようにする方法もあります。
とはいえ、ポリモーフィズムを前提にした抽象クラスでは、仮想デストラクタを用意する考え方が一般的です。
値渡しではなく参照やポインタで扱う
抽象クラスは直接オブジェクトを作れないため、値として扱うことには向いていません。
関数の引数として使う場合も、値渡しではなく、参照やポインタとして扱うのが基本です。
また、オブジェクトの所有権を管理する場合は、スマートポインタを使う設計がよく使われます。
抽象クラスを値として扱おうとすると、インスタンス化できない問題だけでなく、派生クラスの情報が失われるオブジェクトスライシングの問題にもつながります。
コンストラクタやデストラクタから仮想関数を呼ばない
C++では、コンストラクタやデストラクタから仮想関数を呼ぶ設計には注意が必要です。
基底クラスのコンストラクタが実行されている段階では、派生クラス部分の初期化はまだ完了していません。
そのため、コンストラクタ内で仮想関数を呼んでも、期待する派生クラス側の処理が呼ばれないことがあります。
特に、純粋仮想関数をコンストラクタやデストラクタから呼ぶ設計は避けるべきです。
オーバーライド時は関数の形を一致させる
派生クラスで基底クラスの関数をオーバーライドする場合、関数名だけでなく、引数や修飾子なども一致している必要があります。
たとえば、基底クラス側で読み取り専用の関数として定義されている場合、派生クラス側でも同じ条件で定義しなければ、正しくオーバーライドできません。
関数名の打ち間違い、引数の違い、修飾子の不一致などがあると、意図した上書きにならないことがあります。
現代的なC++では、オーバーライドを明示する仕組みを使うことで、このようなミスをコンパイラに検出させることができます。
抽象クラスとインターフェースの関係
C++には、JavaやC#のような専用のインターフェース用キーワードはありません。
その代わり、純粋仮想関数を中心にした抽象クラスを、インターフェースのように使うことがあります。
C++では抽象クラスをインターフェースのように使う
C++では、純粋仮想関数だけを持つ抽象クラスを作ることで、インターフェースに近い役割を持たせられます。
たとえば、「描画できるもの」「保存できるもの」「ログを出力できるもの」といった共通の操作を定義し、それを複数のクラスに実装させることができます。
このような設計により、具体的なクラスに依存しすぎない柔軟なコードを書きやすくなります。
C++の抽象クラスは柔軟性が高い
C++の抽象クラスは、単なるインターフェースよりも柔軟です。
純粋仮想関数だけでなく、通常のメンバ関数、メンバ変数、コンストラクタ、デストラクタ、共通処理などを持たせることができます。
そのため、完全に仕様だけを定義する用途にも使えますし、一部の共通実装を持つ基底クラスとしても使えます。
ただし、柔軟である分、役割を詰め込みすぎると設計が分かりにくくなります。
抽象クラスには、できるだけ明確な責務を持たせることが大切です。
抽象クラスと通常クラスの違い
抽象クラスと通常クラスの大きな違いは、直接オブジェクトを作れるかどうかです。
通常クラスは具体的な処理を持ち、必要な条件を満たしていればオブジェクトを作れます。
一方、抽象クラスは純粋仮想関数を含むため、そのままではオブジェクトを作れません。
通常クラスは具体的なオブジェクトを作るために使う
通常クラスは、具体的なデータや処理を持つクラスです。
たとえば、ユーザー、商品、注文、座標、ファイルなど、具体的な対象を表す場合によく使われます。
通常クラスは、必要なメンバや関数がそろっていれば、そのままオブジェクトとして利用できます。
抽象クラスは共通の仕様を定義するために使う
抽象クラスは、具体的なオブジェクトを作るためではなく、複数の派生クラスに共通する仕様を定義するために使われます。
たとえば、「支払い方法」「図形」「出力先」「描画対象」など、具体的な種類が複数存在するものに対して、共通の操作を定義したい場合に向いています。
抽象クラスは、実体そのものではなく、設計上の共通ルールを表すものです。
抽象クラスを使うべき場面
抽象クラスは、どのような場面でも使えばよいというものではありません。
効果を発揮しやすい場面があります。
複数の派生クラスを共通の型として扱いたい場合
抽象クラスが特に役立つのは、複数の派生クラスを共通の型として扱いたい場合です。
たとえば、円、長方形、三角形をすべて「図形」として扱いたい場合や、クレジットカード決済、QRコード決済、銀行振込をすべて「決済方法」として扱いたい場合です。
このような場面では、抽象クラスを使うことで、呼び出し側の処理を共通化できます。
操作は共通だが処理内容が異なる場合
抽象クラスは、「操作の名前は共通しているが、具体的な処理内容はクラスごとに異なる」という場面に向いています。
たとえば、描画する、保存する、読み込む、支払う、面積を求める、ログを出すといった操作です。
これらは操作としては共通していますが、対象によって実際の処理内容は変わります。
抽象クラスを使うことで、共通の操作を定義しつつ、処理内容を派生クラスごとに変えられます。
将来的に機能を拡張する可能性がある場合
将来的に同じ種類のクラスが増える可能性がある場合も、抽象クラスが役立ちます。
共通のインターフェースを先に定義しておけば、新しい派生クラスを追加しやすくなります。
ただし、将来を見越しすぎて必要以上に抽象化すると、かえって構造が複雑になります。
実際に複数の実装が必要になりそうな場面で使うのが現実的です。
抽象クラスを使わない方がよい場面
抽象クラスは便利ですが、使いすぎるとコードが読みにくくなります。
必要のない場面では、通常のクラスを使う方がシンプルです。
派生クラスが1種類しかない場合
派生クラスが1種類しかなく、今後も増える予定がない場合は、抽象クラスを作る必要性はあまり高くありません。
抽象クラスを作ると柔軟性は増しますが、その分、継承関係やクラス構造が複雑になります。
単純な処理であれば、通常のクラスだけで十分です。
単なるデータ構造を表したい場合
座標、設定値、ユーザー情報、商品情報のように、単にデータをまとめたいだけの場合は、抽象クラスを使う必要はありません。
このような場合は、通常のクラスや構造体を使う方が分かりやすくなります。
抽象クラスは、主に共通の振る舞いや操作を定義したい場合に使うものです。
差し替えや拡張の必要がない場合
処理を差し替える予定がなく、派生クラスを共通の型として扱う必要もない場合は、抽象クラスを使わない方がよいことがあります。
抽象化は設計を柔軟にしますが、同時に読むべきクラスや関係性を増やします。
必要な場面でだけ使うことが、分かりやすい設計につながります。
抽象クラスとテンプレートの違い
C++では、抽象クラスの代わりにテンプレートを使って柔軟な設計を行うこともあります。
抽象クラスとテンプレートは、どちらも複数の型を扱うために使えますが、仕組みや向いている場面が異なります。
抽象クラスは実行時ポリモーフィズム
抽象クラスを使った設計では、実行時に実際のオブジェクトの型に応じて処理が切り替わります。
これを実行時ポリモーフィズムといいます。
異なる種類のオブジェクトを共通の型として扱いたい場合や、プログラムの実行中に処理を差し替えたい場合に向いています。
テンプレートはコンパイル時ポリモーフィズム
テンプレートを使うと、コンパイル時に型に応じた処理が生成されます。
これをコンパイル時ポリモーフィズムといいます。
型がコンパイル時に決まっている場合や、実行時の仮想関数呼び出しを避けたい場合に向いています。
目的に応じて使い分ける
抽象クラスとテンプレートは、どちらが常に優れているというものではありません。
異なる型のオブジェクトを同じまとまりで扱いたい場合は、抽象クラスが向いています。
一方、型ごとに処理を最適化したい場合や、コンパイル時に型を決められる場合は、テンプレートが向いています。
設計の目的に応じて使い分けることが重要です。
実務で覚えておきたいポイント
抽象クラスは、C++のオブジェクト指向設計で重要な役割を持ちます。
特に、保守性や拡張性を意識する場面で役立ちます。
抽象クラスは共通の約束を作るもの
抽象クラスは、派生クラスに対して共通の約束を作るための仕組みです。
「この種類のクラスなら、この操作ができる」という前提を作ることで、呼び出し側のコードをシンプルにできます。
その結果、具体的なクラスへの依存を減らし、変更に強い設計にしやすくなります。
抽象化しすぎないことも大切
抽象クラスは便利ですが、使いすぎると設計が複雑になります。
クラスの数が増え、継承関係が深くなると、コード全体の見通しが悪くなることがあります。
抽象クラスは、複数の実装を共通の型で扱いたい場合や、機能の差し替えが必要な場合に使うと効果的です。
実装を強制したい場面で有効
抽象クラスは、派生クラスに特定の関数の実装を求めたい場合に有効です。
複数人で開発する場合や、一定の設計ルールを守りたい場合にも役立ちます。
特に、プログラム全体で共通の操作を保証したい場合、抽象クラスを使うことで設計の一貫性を保ちやすくなります。
まとめ
C++の抽象クラスは、純粋仮想関数を1つ以上持つクラスです。
抽象クラスは直接オブジェクトを作れませんが、ポインタや参照として使うことで、派生クラスを共通の型として扱えます。
主な役割は、共通インターフェースを定義し、派生クラスに必要な機能の実装を求めることです。
抽象クラスは共通の仕様を定義する
抽象クラスは、複数の派生クラスに共通する仕様を定義するために使います。
具体的な処理は派生クラスに任せ、呼び出し側は共通の操作だけを意識できます。
純粋仮想関数が中心になる
抽象クラスを作るうえで中心になるのが純粋仮想関数です。
純粋仮想関数によって、派生クラスに必要な関数の実装を求めることができます。
また、C++では純粋仮想関数にも実装を持たせることができますが、純粋仮想関数である限り、そのクラスは抽象クラスのままです。
参照やポインタで扱うのが基本
抽象クラスは直接インスタンス化できないため、値として扱うのではなく、参照、ポインタ、スマートポインタで扱うのが基本です。
特に、基底クラス経由で派生クラスのオブジェクトを削除する可能性がある場合は、仮想デストラクタを用意することが重要です。
必要な場面で使うことが重要
抽象クラスは、柔軟で拡張性の高い設計を作るための強力な仕組みです。
ただし、必要以上に使うと設計が複雑になります。
複数の派生クラスを共通の型で扱いたい場合、処理を差し替えたい場合、派生クラスに共通の操作を保証したい場合に使うと効果的です。
以上、C++の抽象クラスについてでした。
最後までお読みいただき、ありがとうございました。
