クラスを拡張して再利用する
あなたはコンストラクタでクラスを組み立て、デストラクタで後始末をしてきました。継承はその次のステップです。あるクラスのメンバを別のクラスにコピーする代わりに、新しいクラスが既存のクラスの特殊化された版であると宣言し、すべてを自動的に受け継がせます。
既存のクラスが基底クラス(親)で、新しいクラスが派生クラス(子)です。派生クラスは基底クラスのすべてのデータと振る舞いから出発し、そこに違いとなる部分を追加したり変更したりします。これはC++が「is-a」関係を表現するための主要な道具です。Dog は Animal であり、SavingsAccount は BankAccount です。
基本構文:class Derived : public Base
派生クラス名のあとにコロン、アクセス指定子、基底クラス名を続けることで継承します。最も一般的な形はpublic継承です。
Dog は name も eat() も一切宣言していませんが、どちらも Animal から継承されているため Dog オブジェクトで動作します。派生クラスは、基底クラスがまったく知らない bark() のようなメンバを自由に追加できます。
protected:子クラス専用のメンバ
基底クラスの private メンバは派生クラスの内部からもアクセスできません。継承はカプセル化を壊しません。基底クラスの内部実装を外部からは隠しつつサブクラスには使わせたいときは、アクセス指定子 protected を使います。
3つのレベルを同心円としてイメージしてください。private は「このクラスだけ」、protected は「このクラスとその子孫」、public は「すべての人」です。全体像についてはアクセス指定子を参照してください。
コンストラクタとデストラクタの順序
派生オブジェクトは基底クラスのサブオブジェクトを内部に含んでおり、派生部分が組み立てられる前に、その基底部分が生きている必要があります。そのため、構築は基底クラスから先に、破棄は派生クラスから先に(ちょうど逆順で)行われます。基底クラスがコンストラクタの引数を必要とする場合は、メンバ初期化リストを通じて渡します。
出力がこの順序を具体的に示します。
Animal ctor: Rex // 基底クラスが先に構築される
Dog ctor // 次に派生部分
Dog dtor // 逆順で破棄される...
Animal dtor: Rex // ...基底クラスが最後
: Animal(n) を書き忘れ、しかも基底クラスにデフォルトコンストラクタがない場合、コードはコンパイルできません。C++は基底部分をどう組み立てればよいか分からないからです。継承の対象にするつもりの基底クラスは、ほぼ常にデストラクタを宣言すべきです(そして、次のページで示すように、多くの場合は仮想デストラクタを)。
オーバーライド:基底クラスのメソッドを再定義する
派生クラスは、同じシグネチャを持つメソッドを宣言することで、継承したメソッドを置き換えられます。元のものには Base::method() を通じて引き続きアクセスできます。
これは単なる*名前の隠蔽(name hiding)*であって、ポリモーフィズムではありません。どの describe() が実行されるかは、変数の静的な型によってコンパイル時に決まります。これは重大な制限です。実際には Circle を指している Shape& や Shape* を通じて呼び出しても、やはり Shape::describe() が実行されてしまいます。これを解決するには virtual が必要で、それが次のページのテーマです。
オブジェクトスライシングに注意
基底クラスの参照やポインタは派生オブジェクトを指せるため、派生オブジェクトを基底型の変数にコピーしたくなります。やめましょう。派生部分が切り落とされてしまいます。
a は基底クラスのラベルを貼った Dog ではなく、正真正銘の Animal なので、breed はそこには存在しません。派生オブジェクトを多態的に扱うには、基底クラスの参照(Animal&)かポインタ(Animal*)を使う必要があり、基底クラスの値を使ってはいけません。スライシングは静かに起こります。問題なくコンパイルされ、ただこっそりデータを捨てるだけなので、本番環境まで紛れ込みやすい継承のバグの一つです。
よくある間違いと避け方
- 基底クラスの
privateメンバが子クラスから触れると思い込む。 触れません。派生クラスが正当に必要とするデータにはprotectedを使い、本当に内部だけの状態はprivateのままにしてください。 - 基底クラスのコンストラクタ引数の転送を忘れる。 基底クラスにデフォルトコンストラクタがない場合は、派生クラスのコンストラクタの初期化リストで明示的に呼び出す必要があります(
: Base(args))。 - 派生オブジェクトを基底クラスの値にスライスしてしまう。
DogをAnimalにコピーすると、Dog固有のものはすべて失われます。代わりに基底クラスの参照やポインタを渡し、保持してください。 - コード再利用のために継承を使いすぎる。 継承は「is-a」をモデル化します。関係が実際には「has-a(〜を持つ)」である場合(
CarはEngineを持つ)、継承よりもコンポジション(メンバオブジェクト)を選びましょう。
次へ:仮想関数
先ほど見たオーバーライドはコンパイル時に解決されたため、基底ポインタを通じた呼び出しは派生版を無視しました。次のページ、仮想関数では、キーワード virtual と override を紹介します。これは実行時の型にどのメソッドを実行するかを決めさせる仕組みで、真のポリモーフィズムを可能にし、なぜ基底クラスに仮想デストラクタが必要なのかを説明します。
よくある質問
C++における継承とは何ですか?
継承を使うと、既存のクラス(基底クラス)をもとに新しいクラス(派生クラス)を定義できます。派生クラスは基底クラスのデータメンバとメンバ関数を自動的に受け継ぎ、新しいものを追加したり既存の振る舞いを置き換えたりできます。これは「is-a(〜は〜である)」の関係をモデル化し(Dog は Animal である)、C++がクラス階層をまたいでコードを再利用・拡張する主要な手段です。
C++におけるpublic継承とprivate継承の違いは何ですか?
public継承(class Dog : public Animal)では、基底クラスのpublicインターフェースが派生クラスでもpublicのまま保たれるため、Dog は Animal であるとみなされ、Animal が期待されるあらゆる場所で使えます。private継承では、受け継いだメンバがprivateになり、派生クラスは基底クラスの実装を再利用しますが、その代わりとして使うことはできません。public継承が圧倒的に一般的なケースで、privateは「〜を用いて実装する」という再利用のためだけに使います。
C++の継承では、コンストラクタとデストラクタはどの順序で実行されますか?
コンストラクタは基底クラスから先に実行されます。派生クラスのコンストラクタ本体が実行される前に、基底クラスが完全に構築されます。デストラクタはちょうど逆の順序で実行されます。先に派生クラス、次に基底クラスです。これにより、派生オブジェクトが構築または破棄される間、それが依存するすべての部分がすでに存在している(あるいはまだ存在している)ことが保証されます。