継承と仮想関数
オブジェクト指向の授業での、前回までのカプセル化の次の段階として、 継承について説明を行う。
継承(派生)
データを扱っている際に、基本的な部分では共通性があるけど、 様々なバリエーションが存在するデータには、継承(派生)が使われる。
例として人の情報に、親子関係を表すデータを追加する場合を考える。 これを構造体で実装すると、追加されたデータ側で、同じような処理を 沢山書く必要がでてくる。
// C言語ベースで破綻する例 // 元となるデータ構造 struct Person { char name[ 10 ] ; int age ; } ; void setPerson( struct Person* p , char* s, int a ) { strcpy( p->name , s ) ; p->age = a ; } void printPerson( struct Person* p ) { printf( "%s %d" , p->name , p->age ) ; } // 子どもの情報をもつ拡張したデータ struct Parent { struct Person base ; struct Person* child ; } ; void setParent( struct Parent* p , char* s , int a , struct Person* c ) { setPerson( p , s , a ) ; child->child = c ; } void main() { struct Person mitsuki ; setPerson( &mitsuki , "mitsuki" , 12 ) ; struct Parent tohru ; setParent( &tohru , "tohru" , 47 , &mitsuki ) ; printPerson( &mitsuki ) ; printParent( &tohru ) ; // printParent() を書く必要あり。 // 名前と年齢を表示したいだけなのに... }
これと同じようなプログラムは、C++であれば派生を使って簡単にかける。
class Person { // 基底クラス private: char name[ 10 ] ; int age ; public: Person( char*s , int a ) { strcpy( name , s ) ; age = a ; } void print() { printf( "%s %d" , name , age ) ; } } ; class Parent : public Person { // 派生クラス private: Person* child ; public: Parent( char* s , int a , Person* c ) : Person( s , a ) // 基底クラスのコンストラクタの呼び出し { child = c ; } } ; void main() { Person mitsuki( "mitsuki" , 12 ) ; Parent tohru( "tohru" , 47 , &mitsuki ) ; mitsuki.print() ; tohru.print() ; // 継承により"tohru 47"が表示できる }
上記のtohru.print() の様に、基底クラスの同名メソッドを流用できることを、 継承と呼ぶ。
仮想関数
上記の派生クラスで、Parentを表示する時に、子どもも一緒に表示させたければ、 以下の様に派生クラスでprint()を再定義しても良い。
class Parent : public Person { : public: void print() { Person::print() ; // 基底クラスメソッド呼び出し child->print() ; } } ; void main() { : tohru.print() ; // "tohru 47 mitsuki 12" の表示。
しかし、親子関係がもう1世代加わるとどうなるか?
void main() { Person mitsuki( "mitsuki" , 12 ) ; Parent tohru( "tohru" , 47 , &mitsuki ) ; Parent kinmatsu( "kinmatsu" , 78 , &tohru ) ; // 型の不一致? }
本来なら、&tohru は、Parent*型。 Parent::Parent(char*,int,Person*)コンストラクタ第3引数は、Person*型で型が違う。 しかし、オブジェクト指向では、 派生クラスのポインタは、安全に基底クラスのポインタに暗黙の変換が可能なので、 問題は無い。
一方で、以下の命令を実行した場合、何が表示されるのか?
kinmatsu.print() ; // kinmatsu 78 tohru 47 までしか表示されない。
これは、Parent型のchildは、Person型であり、tohruを表示しようとしても、 すでに"Person"の子供なしと思われてしまっている。 ここで、tohru は、元々Parent型であり、孫のmitsuki まで表示したい場合は、 どうすべきか?データ自信が、自分は"Person"なのか"Parent"なのか知っていれば、都合がよい。こういう場合は、仮想関数にて宣言する。
class Person { : private: virtual void print() { printf( "%s %d" , name , age ) ; } } ; class Parent : public Person { : private: virtual void print() { Person::print() ; child->print() ; // 仮想関数呼び出し } } ;
この例では、kinmatsu.print() を実行する時に、child->print() を実行する場合には、 tohruは、Parent型なので、Parent::print() を呼び出し、tohru.print()の時には、 mitsukiは、Person型なので、Person::print() を呼び出してくれる。
"virtual"を使うと、データ構造の中に、型情報が自動的に埋め込まれるため、 このような、データに応じた処理の呼び分けが可能となる。
質問で、virtual のキーワードを一方にしか付けなかったらどうなるの? ということで、調べてみた。 基底クラスで virtual 付き、派生クラスで virtual 無しだと、両方に付けた時と同じ動きとなった。資料をみると、派生クラスでは virtual は書かなくてもいいとのことであった。 一方、基底クラスで virtual 無し、派生クラスで virtual 有りだと、 処理の呼び分けができなかった。 うーん昔、書き間違えてvirtual忘れた時には、エラーが出た記憶があるのだが…