ホーム » スタッフ » 斉藤徹 » 継承と仮想関数

2012年5月
« 4月   6月 »
 12345
6789101112
13141516171819
20212223242526
2728293031  

最近の投稿(電子情報)

アーカイブ

カテゴリー

継承と仮想関数

オブジェクト指向の授業での、前回までのカプセル化の次の段階として、 継承について説明を行う。

継承(派生)

データを扱っている際に、基本的な部分では共通性があるけど、 様々なバリエーションが存在するデータには、継承(派生)が使われる。

例として人の情報に、親子関係を表すデータを追加する場合を考える。 これを構造体で実装すると、追加されたデータ側で、同じような処理を 沢山書く必要がでてくる。

// 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忘れた時には、エラーが出た記憶があるのだが…