ホーム » 「仮想関数」タグがついた投稿

タグアーカイブ: 仮想関数

2018年12月
« 11月    
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

最近の投稿(電子情報)

アーカイブ

カテゴリー

図形と仮想関数の継承方法

純粋仮想基底クラスと図形の課題の基本形

課題で取り組んでいるプログラムは、純粋仮想基底クラスFigureと、そこから派生させたクラスと仮想関数で絵を書いている。このような派生の関係を以下のような図で表現する。

class Figure {
public:
   virtual void draw( int x , int y ) = 0 ;
} ;

class FigureBox : public Figure {
private:
   int width , height ;
public:
   FigureBox( int w , int h ) : width( w ) , height( h ) {}
   virtual void draw( int x , int y ) {
      // 四角を描く処理
   }
} ;

class FigureCircle : public Figure {
private:
   int radius ;
public:
   FigureCircle( int r ) : radius( r ) {}
   virtual void draw( int x , int w ) {
      // 円を描く処理
   }
} ;

色付き図形を派生する方法

課題では、上記プログラムを活用して、色付き図形のクラスを作ることを目的とするが、その実装方法には色々な方法がある。

単純なやり方は、FigureBox と同じように、Figure から FigureColorBox を派生させる方法だろう。

class FigureColorBox : public Figure {
private:
   int width , height , color ;
public:
   FigureColorBox( int w , int h , int c )
   : width( w ) , height( h ) , color( c ) {}
   virtual void draw( int x , int y ) {
      // 色を変える処理
      // 四角を描く処理 ... FIgureBox と同じ処理
   }
} ;

この方法は単純だけど、四角を描く処理を書くため無駄であり、FIgureBox と処理を共通化できればプログラムを書く手間を減らせるはず。

処理を共通化するなら派生すればいい

class FigureColorBox : public FigureBox {
private:
   int color ;  // 幅と高さの記述が無い
public:
   FigureColorBox( int w , int h , int c )
   : FigureBox( w , h ) , color( c ) {}
   virtual void draw( int x , int y ) {
      // 色を変える処理
      FigureBox::draw( x , y ) ; // 親クラスの処理を継承
   }
} ;

同じような色の処理を追加したクラスが沢山ある場合

上記の、FigureBox から FigureColorBox を派生させた場合は、四角を描く処理が継承により共通化ができた。

しかし、同じように FigureCircle から FigureColorCircle を派生させる…といったクラスを沢山作る場合は、色を変える処理を何度も使うことになるが、処理の共有することができない。こういった場合は、以下のような方法がある。

class Color {
private:
   int color ;
public:
   Color( int c ) : color( c ) {}
   void set_color() {
      // 色を変える処理
   }
} ;
class FigureColorBox : public FigureBox , public Color {
public:                // 多重継承のキモ
   FigureColorBox( int w , int h , int c )
   : FigureBox( w , h ) , Color( c ) {}
   virtual void draw( int x , int y ) {
      Color::set_color() ;       // Colorクラスを使って色を設定
      FigureBox::draw( x , y ) ; // FigureBoxクラスで形を描く
   }
} ;

多重継承

上記の FigureColorBox のように、親クラスとして、FigureBox と Color のように複数のクラスをもつ継承は、多重継承と呼ばれる。

ただし、多重継承は後に示すような曖昧さや、実装の際の手間を考えると、多重継承は必ずしも便利な機能ではない。このため、オブジェクト指向のプログラミング言語によっては、多重継承機能が使えない。

Java では、多重継承が使えない代わりに、interface 機能が使えたりする。

ダイヤモンド型の継承と曖昧さ

C++ のような言語での多重継承が問題となる事例として、ダイヤモンド型の継承が挙げられる。

例えば、動物クラスから鳥クラス・哺乳類クラスを派生して、鳥クラスからニワトリを派生して、哺乳類クラスから人間を派生するというのは、進化の過程を踏まえると、自然な派生と考えられる。

しかし、鳥がクラスメンバーとして羽と足を持ち、哺乳類が手と足を持つとしたとき、鳥のくちばしを持ち卵を生むカモノハシを派生する場合には、どうすれば良いのだろうか?しかし、これを鳥と哺乳類を親クラスとした多重継承で実装をすると、手と羽と足が4本のカモノハシができてしまう。(お前はドラゴンかッ!)

また、大元の動物クラスがインスタンスを持つ場合、このような多重継承をすると、同じ名前のインスタンスを2つ持つことになる。この場合、C++では仮想継承というメカニズムを使うことができる。

class Animal {
private:
   char  name[10] ;
} ;
class Bird : public virtual Animal {
} ;
class Mammalian : public virtual Animal {
} ;
class Psyduck : public Bird , public Mammalian {
   // このクラスは、name は1つだけ。 
} ;

また、動物が動くためのmove()というメソッドを持つとした場合、鳥は羽で飛び、哺乳類は足で移動する処理となるだろう。しかし、多重継承のカモノハシは、鳥の羽で動くメソッドmove()と、哺乳類の足で動くメソッドmove()を持つことになり、カモノハシに動け…と命令した場合、どちらのメソッドmove() を使うのだろうか?

こういった、使いにくさ・実装時の手間・処理の曖昧さを考慮したうえで、多重継承のメカニズムが使えるオブジェクト指向プログラム言語は少ない。

仮想関数

仮想関数

前回の派生したプログラムで継承の説明をしたが、以下のようなプログラムでは、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のような基底クラスは、純粋仮想基底クラスなどと呼ばれる。