前々回の講義では派生と継承の説明にて、次のようなプログラムを説明した。
派生と継承の復習
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char* s , int a ) { strcpy( name , s ) ; age = a ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス class Student : public Person { private: char dep[ 10 ] ; int grade ; public: Student( const char* s , int a , const char* d , int g ) : Person( s , a ) { strcpy( dep , d ) ; grade = g ; } void print() { // 継承でなくStudent版のprintを定義 Person::print() ; printf( "= %s %d\n" , dep , grade ) ; } } ; int main() { // 派生と継承の確認 Person tsaitoh( "tsaitoh" , 50 ) ; tsaitoh.print() ; Student naka( "naka" , 22 , "PS" , 2 ) ; naka.print() ; // Student版を実行 // 異なる型のデータをごちゃ混ぜにできないか? Person* table[2] ; table[0] = &tsaitoh ; table[1] = &naka ; for( int i = 0 ; i < 2 ; i++ ) { table[i]->print() ; } return 0 ; }
このプログラムのmain() では、tsaitohとnakaのデータを 表示させているが、tsaitohとnakaは、オマケの有る無しの違いはあっても、 元をたどれば、Person型。ごちゃまぜにできないだろうか?
int main() { : // 異なる型のデータをごちゃ混ぜにできないか? Person* table[2] ; table[0] = &tsaitoh ; table[1] = &naka ; for( int i = 0 ; i < 2 ; i++ ) { table[i]->print() ; } return 0 ; }
C++では、派生クラスは格下げして基底クラスのように振る舞うこともできる。 上記の例では、Studentのポインタ型である、&nakaは、 Personのポインタ型として振る舞うこともできる。
ただし、table[]に格納する時点で、Person型へのポインタなので、 table[i]->print() を呼び出しても、Personとして振る舞うため、 tsaitoh 50 / naka 22 が表示される。
仮想関数
上記のプログラムでは、Student型には、名前,年齢,所属,学年をすべて表示する Student::print() が宣言してある。 であれば、「naka 22」 ではなく、「naka 22 PS 2」と表示したくはならないか?
上記の例では、table[] に格納する段階で、格下げされているため、「naka 22」しか表示できない。 しかし、以下のように書き換えると、「naka 22 PS 2」と表示できる。
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char* s , int a ) { strcpy( name , s ) ; age = a ; } virtual void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス class Student : public Person { private: char dep[ 10 ] ; int grade ; public: Student( const char* s , int a , const char* d , int g ) : Person( s , a ) { strcpy( dep , d ) ; grade = g ; } virtual void print() { // 継承でなくStudent版のprintを定義 Person::print() ; printf( "= %s %d\n" , dep , grade ) ; } } ; int main() { Person tsaitoh( "tsaitoh" , 50 ) ; Student naka( "naka" , 22 , "PS" , 2 ) ; // 異なる型のデータをごちゃ混ぜにできないか? Person* table[2] ; table[0] = &tsaitoh ; table[1] = &naka ; for( int i = 0 ; i < 2 ; i++ ) { table[i]->print() ; } return 0 ; }
上記の例の用に、printメソッドに virtual 宣言を追加すると、 C++では、tsaitoh,naka のデータ宣言時に、データ種別情報が埋め込まれる。 この状態で、table[i]->print() を呼び出されると、 データ種別毎のprint()を呼び出してくれる。 (この実装では、前回授業の関数ポインタが使われている)
ここで、学生さんから、「table[] の宣言をStudentで書いたらどうなるの?」という 質問がでた。おもわず『良い質問ですねぇ〜』と池上彰ばりの返答をしてしまった。
Student* table[] ; table[0] = &tsaitoh ; // 文法エラー // 派生クラスは基底クラスにはなれるけど、 // 基底クラスが派生クラスになることはできない。 table[1] = &naka ;
純粋仮想基底クラス
// 純粋仮想基底クラス class Object { public: virtual void print() = 0 ; // 中身の無い純粋基底クラスを記述しない時の書き方。 } ; // 整数データの派生クラス class IntObject : public Object { private: int data ; public: IntObject( int x ) { data = x ; } virtual void print() { printf( "%d\n" , data ) ; } } ; // 文字列の派生クラス class StringObject : public Object { private: char data[ 100 ] ; public: StringObject( const char* s ) { strcpy( data , s ) ; } virtual void print() { printf( "%s\n" , data ) ; } } ; // 実数の派生クラス class DoubleObject : public Object { private: double data ; public: DoubleObject( double x ) { data = x ; } virtual void print() { printf( "%lf\n" , data ) ; } } ; // 動作確認 int main() { Object* data[3] = { new IntObject( 123 ) , new StringObject( "abc" ) , new DoubleObject( 1.23 ) , } ; for( int i = 0 ; i < 3 ; i++ ) { data[i]->print() ; } return 0 ; } ;
この書き方では、data[]には、整数、文字列、実数という異なるデータが入っているが、 Objectという純粋仮想基底クラスを通して、共通な型のように扱えるようになる。 そして、data[i]->print() では、各型の仮想関数が呼び出されるため、 「123 abc 1.23」 が表示される。
ここで、Object の様な一見すると中身が何もないクラスを宣言し、 このクラスから様々な派生クラスを用いるプログラムテクニックは、 広く利用され、Objectのような基底クラスは、純粋仮想基底クラスなどと呼ばれる。