継承には「単一継承」「多重継承」「仮想継承」の三つの方法がありますが、最初に単一継承から説明することにします。単一継承はもっとも単純な継承ですが、多くの場合はこれで十分です。JAVA というオブジェクト指向言語などは、単一継承しかサポートしていませんが、それでも既にネットワーク上で動作するプログラムなどを中心に、幅広く採用されていることはご存知かと思います(携帯電話のアプリさえも!)。
話を簡単にするために、ここではメンバ変数しかないクラスを考えます(単純な、単一継承)。クラスに「仮想関数」というメンバ関数が加わると、話は少し複雑になりますが(複雑な、単一継承)、それは次回以降で説明します。まずは普通の、何からも派生していない(何も継承していない)クラスが、メモリ上ではどうなっているのかを見てみましょう。
class cA { public: int a1; int a2; }; /* cA のメモリレイアウト [ a1 ][ a2 ] *[ ] は 32bit */ /* C 風に書くなら */ struct sA { int a1; int a2; };
cA と sA の違いは、ここでは何もありません。cA のように public なメンバ変数しか持たない独立したクラスは、構造体と同じであるといってもよいでしょう。では次に、cA の機能を(単一)継承する class cB を作ってみましょう。
class cB : public cA { public: int b1; int b2; }; /* [ a1 ][ a2 ][ b1 ][ b2 ] // cB on mem | cA | | cB | */ /* C-Style cB */ struct sB { struct sA a; int b1; int b2; };
メモリレイアウトを見ると、cB の基底クラスである cA のメンバ変数 a1, a2 の後に、cB のメンバ変数 b1, b2 が配置されています。構造体 sB として cB を表現した場合、sA 型のメンバ a の後に b1, b2 と続くことになります(実際には、a は無名となるべきです)。結果、cB は cA::a1, cA::a2, cB::b1, cB::b2 の 4 つのメンバを持つことになります。cB からさらに cC を派生させた場合も、同じようにメンバが継承されます。
class cC : public cB { public: int c1; int c2; }; /* [ a1 ][ a2 ][ b1 ][ b2 ][ c1 ][ c2 ] | cB | | cC | */ struct sC { struct sB b; int c1; int c2; };
次に、ポインタの話をしたいと思います。C++ では、ポインタの型と、それが指しているアドレスに存在するオブジェクトの型とが一致しない場合があります。
cB b; cA *pa = &b;
上の例では、cA* 型のポインタが cB 型のオブジェクトを指しています。通常、cA* 型ポインタは cA 型オブジェクトを指すべきで、cB を指すのは危険です。しかし、「 cA* からは cA のメンバにしかアクセスできない」ことを思い出してください。つまり、cA *pa を使ったアクセスでは、
pa+0 pa+4 | | [ a1 ][ a2 ][ b1 ][ b2 ]
この図のように、a1 と a2 にしかアクセスできません。逆に言えば、a1, a2 の先に何があっても構わないということになります。つまり、cA* pa が指しているオブジェクトが、cA から派生したオブジェクトである限り、pa は安全だということです。この関係は、cA と cC の間、cB と cC の間にも成り立ちます。
cB b; cC c; cA *pa = &c; cB *pb = &c; /* pa+0 pa+4 | | [ a1 ][ a2 ][ b1 ][ b2 ][ c1 ][ c2 ] pb+0 pb+4 pb+8 pb+12 | | | | [ a1 ][ a2 ][ b1 ][ b2 ][ c1 ][ c2 ] */
まとめると、基底クラスのポインタが、派生クラスのオブジェクトを指すのは合法だということになります。ではこの逆はどうでしょうか。つまり、派生クラスのポインタが、基底クラスのオブジェクトを指したらどうなるでしょうか。
cA a; cB *pb = &a; /* pb+0 pb+4 pb+8 pb+12 | | | | [ a1 ][ a2 ][????][????] */
pb は、それが指すオブジェクトに a1, a2, b1, b2 があると思っていますが、しかし実際に指しているのは cA 型オブジェクト a なので、a1, a2 しか存在しません。よって pb->b1 とすると、a2 の向こう側の未知の領域にアクセスしてしまうことになり、予測不能な結果をもたらすことになります。しかも pb->b1 自体は明らかに合法であるため、コンパイルエラーは起こらず、実行して初めて問題が起こることになります。それを防ぐには pb = &a を禁止するのが手っ取り早い方法で、あるコンパイラは上のコードの場合「 cA* から cB* には、(暗黙的には)変換できない」という旨のエラーを報告するようになっています。
一方、pb に代入される cA* 型ポインタの指しているのが cB 型オブジェクトであれば問題ありません。つまり、
cB b; cA *pa = &b; . . . cB *pb = ( cB* ) pa;
とした場合は、何も問題は起こりません。この場合の pa は明らかに cB b を指しているため、pb = pa は pb = &b と同じことになります。ただ、このような短いコードであれば pa が指しているオブジェクトの型は cB だと把握できますが、プログラムが複雑になってくると pa が指している(指すべき)オブジェクトの型が何であるかわかりにくくなってきます。そのこともあって、「実行時に、あるポインタが指すオブジェクトの型を調べる機構」があれば便利だと考えた人は多く、色々な人が色々な方法で勝手にその機構を実装したため、互換性の問題が生じるようになりました。そこで現在の C++ では Run-Time Type Information ( RTTI ) という仕組みが導入されていますが、これについての説明は専門書に委ねることにします。私は使ったことがありません(実行時にいちいち型を確認してたら、せっかくのポリモーフィズムが台無しだと思うからです)。
最初のほうで説明した、( cA *) cB* のような「派生ポインタから基底ポインタ」への変換を「アップキャスト」、逆に ( cB* ) cA* のような「基底ポインタから派生ポインタ」への変換を「ダウンキャスト」といいます。アップキャストは安全であり、暗黙に変換されますが、ダウンキャストはこれまで述べてきたように安全ではなく、明示的に変換する必要があります。アップ・ダウンというのは、クラスの関係において基底クラスを「上位」、派生クラスを「下位」とみなすため、ポインタを基底のほうに移すことを「アップキャスト」、派生のほうに移すことを「ダウンキャスト」と呼ぶようになったわけです。
ところでアップ・ダウンにかかわらず、ポインタをキャストしてもそのアドレスは変化しないことに気がついたでしょうか。変化しなくていいのは、メモリ上に「基底 -> 派生」とメンバが並ぶので、派生クラスのオブジェクトの先頭アドレスは、常にその基底クラスの先頭アドレスと一致するからです。これが「派生 -> 基底」だったら、キャストするたびにアドレスを増減させる必要が生じるなど、複雑な問題を抱えることになります。
単一継承の仕組みとキャストの方法がわかったところで、次回はこれに仮想関数を加えてみたいと思います。