ホーム » 2015 » 6月 » 02

日別アーカイブ: 2015年6月2日

2015年6月
« 5月   7月 »
 123456
78910111213
14151617181920
21222324252627
282930  

最近の投稿(電子情報)

アーカイブ

カテゴリー

仮想関数と純粋仮想基底クラス

前々回の講義では派生と継承の説明にて、次のようなプログラムを説明した。

派生と継承の復習

// 基底クラス
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のような基底クラスは、純粋仮想基底クラスなどと呼ばれる。

関数ポインタと仮想関数への導入

派生と継承の基本の説明をしてきたので、次に説明する予定の仮想関数の導入として、 実装のために使われている関数ポインタを紹介。

関数ポインタ

関数ポインタとは、関数へのポインタであり、ポインタを変更することで、 処理を切り替えるために使われる。 まずは、動作説明の簡単なプログラムを紹介。

int add( int x , int y ) { // 加算関数
return x + y ;
}
int mul( int x , int y ) { // 乗算関数
return x * y ;
}
void main() {
int (*f)(int,int) ; // int×2引数、返り値intの関数へのポインタ
f = add ;
printf( "%d" , (*f)( 2 , 3 ) ) ; // 5を表示
f = mul ;
printf( "%d" , (*f)( 2 , 3 ) ) ; // 6を表示
}

関数ポインタを利用すれば、異なるデータに対する処理を、 汎用性高く作ることも可能となる。 例えば以下の vmax() 関数は、自分で用意した大小を比較するだけの関数を渡し、 それ以外の最大値を求めるための処理を行う。 このため、他のデータの最大値を求めたい場合でも、最大値を求める処理を すべて記載するのではなく、対象データの比較関数だけを記述すれば良い。

int intcmp( int* x , int* y ) { // 整数比較関数
if ( *x > *y )
return 1 ;
else if ( *x < *y )
return -1 ;
else
return 0 ;
}
int vmax( void* array ,    // 配列先頭アドレス
int size ,       // 配列データ件数
int sizeofdata , // 1件あたりのbyte数
int(*f)( void*,void* ) ) { // 比較関数
int i , max = 0 ;
for( i = 0 ; i < size ; i++ )
if ( (*f)( array + max * sizeofdata ,
array + i   * sizeofdata ) )
max = i ;
return max ;
}
int idata[ 4 ] = { 11 , 33 , 22 , 44 } ;
char sdata[ 4 ][ 4 ] = { "ab" , "bc" , "aa" , "c" } ;
void main() {
int m ;
// intcmp関数を使って、idata から最大値を探す
m = vmax( idata , 4 , sizeof(int) , intcmp ) ;
printf( "%d" , idata[ m ] ) ;
// strcmp関数を使って、sdata から最大値を探す
m = vmax( sdata , 4 , sizeof(sdata[0]) , strcmp ) ;
printf( "%s" , sdata[ m ] ) ;
}