2018年6月
« 5月    
 12
3456789
10111213141516
17181920212223
24252627282930

最近の投稿(電子情報)

アーカイブ

カテゴリー

仮想関数

仮想関数

前回の派生したプログラムで継承の説明をしたが、以下のようなプログラムでは、Student 型が混在した family[] の配列でも、Person へのポインタに「格下げ」されて保存されているため、
family[i]->print() では、Student 型でも Person型で表示処理が行われる。

class Student : public Person {
   :
   void print() {
      Person::print() ;                    // 名前と年齢を表示
      printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示
   }
} ;
void main() {
   Person saitoh( "t-saitoh" , 53 ) ;
   saitoh.print() ; // t-saitoh 53

   Student mitsu( "mitsuki" , 18 , "E" , 4 ) ;
   Student ayuka( "ayuka" , 16 , "EI" , 2 ) ;
   mitsu.print() ; // mitsuki 18 / E 4   名前,年齢,所属,学年を表示
   ayuka.print() ; // ayuka 16 / EI 2    名前,年齢,所属,学年を表示

   Person* family[] = {
      &saitoh , &mitsu , &ayuka ,  // 配列の中に、Personへのポインタと
   } ;                             // Studentへのポインタが混在している
                                   // 派生クラスのポインタは、
                                   // 基底クラスのポインタとしても扱える
   for( int i = 0 ; i < 3 ; i++ )
      family[ i ]->print() ;       // t-saitoh 53/mitsuki 18/ayuka 16
}                                  //  が表示される。

しかし、Student型とPerson型の機能を活かせないだろうか?

仮想関数

このような場合、オブジェクト指向では、仮想関数の機能が便利である。

class Person {
   :
   virtual void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
class Student : public Person {
   :
   virtual void print() {
      Person::print() ;                    // 名前と年齢を表示
      printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示
   }
} ;
void main() {
   Person saitoh( "t-saitoh" , 53 ) ;
   saitoh.print() ; // t-saitoh 53

   Student mitsu( "mitsuki" , 18 , "E" , 4 ) ;
   Student ayuka( "ayuka" , 16 , "EI" , 2 ) ;
   mitsu.print() ; // mitsuki 18 / E 4   名前,年齢,所属,学年を表示
   ayuka.print() ; // ayuka 16 / EI 2    名前,年齢,所属,学年を表示

   Person* family[] = {
      &saitoh , &mitsu , &ayuka ,
   } ;
   for( int i = 0 ; i < 3 ; i++ )
      family[ i ]->print() ;       // t-saitoh 53/mitsuki 18,E 4/ayuka 16,EI 2
}

仮想関数が宣言されると、基底クラスの中に「型情報(PersonなのかStudentなのか)」が自動的に埋め込まれる。仮想関数を呼び出すと、型情報を使って、Person::print()を呼び出すか、Student::print()を呼び出すか、を選んでくれる。

仮想関数が生まれた背景

仮想関数は、GUI のプログラム記述に向いていた。例えば、GUIシステムでは、画面にデータを表示するための基本型として、座標と幅,高さの情報を持つ Window 型がある。

また、画面のアイコンは、Window型に、表示する絵の画像を追加した派生クラス WindowIcon 型、画面のテキストは、Windows型に、表示するテキストやフォント情報を持った、WindowText といった型のようなものとする。そうなると仮想関数の概念が無いと、display() を呼び出すためには、派生型の種類が大量ならばデータの型に応じた if 文を大量に書き並べないといけないし、データ型を区別するための情報(下記の例ならばtype)に、型毎に異なるマジックナンバーを埋め込む処理を自分で書かないといけない。

class Window {  // 概念を説明するための例にすぎない
private:
    int x , y , w , h ;
    int type ; // 型を区別するための情報
public:
    void display() {
        // 表示する処理
    }
} ;
class WindowIcon : public Window {
private:
    Image  img ;
public:
    void display() {
        // 画像を表示する処理
    }
} ;
class WindowText : public Window {
private:
    Font  font ;
    char* text ;
public:
    void display() {
        // テキストを表示する処理
    }
} ;
void main() {
    WindowIcon wi( アイコンのコンストラクタ ) ;
    WindowText wt( テキストのコンストラクタ ) ;
    Window* wins[] = {
        &wi , &wt , ...
    } ;
    for( int i = 0 ; i < 配列すべて ; i++ ) {
        if ( wins[i]->type が アイコンならば )
            wins[i]->display() ; // アイコンを表示する
        else if ( wins[ i ]->type が テキストならば )
            wins[i]->display() ; // テキストを表示する。
        else if ....
        :
    }
}

関数ポインタ

では、仮想関数はどのようなテクニックを用いて実装されているのだろうか?

これには、関数ポインタが用いられる。

int add( int x , int y ) {
    return x + y ;
}
int mul( int x , int y ) {
    return x * y ;
}
int main() {
    int (*func)( int , int ) ;
    func = add ; // add() ではない
    printf( "%d¥n" , (*func)( 3 , 4 ) ) ; // 7
    func = mul ;
    printf( "%d¥n" , (*func)( 3 , 4 ) ) ; // 12
    return 0 ;
}

仮想関数を用いると、基底クラスにはクラス毎の仮想関数への「関数ポインタ」などの型情報を保存する場所が自動的に作られ、基底クラス・派生クラスが使われると、そのオブジェクト毎に型情報を初期化する処理が行われる。仮想関数が呼び出されると、関数ポインタにより、各型毎のメソッドが呼び出されるため、大量の if 文は不要となる。

純粋仮想基底クラス

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