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

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

2023年11月
 1234
567891011
12131415161718
19202122232425
2627282930  

検索・リンク

派生と継承と仮想関数

前回の派生と継承のイメージを改めて記載する。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ; 
   saitoh.print() ; // 表示 t-saitoh 55
   yama.print() ;   // 表示 yamada 21
                    //      - ES 1
   nomu.print() ;   // 表示 nomura 22
   return 0 ;       //      - PS 2
}

このような処理でのデータ構造は、次のようなイメージで表される。

派生クラスでの問題提起

基底クラスのオブジェクトと、派生クラスのオブジェクトを混在してプログラムを記述したらどうなるであろうか?
上記の例では、Person オブジェクトと、Student オブジェクトがあったが、それをひとまとめで扱いたいこともある。

以下の処理では、Person型の saitoh と、Student 型の yama, nomu を、一つの table[] にまとめている。

int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[ i ]->print() ;
   }
   return 0 ;
}

C++では、Personへのポインタの配列に代入する時、Student型ポインタは、その基底クラスへのポインタとしても扱える。ただし、このように記述すると、table[] には、Person クラスのデータして扱われる。

このため、このプログラムを動かすと、以下のように、名前と年齢だけが3人分表示される。

t-saitoh 55
yamada   21
nomura   22

派生した型に応じた処理

上記のプログラムでは、 Person* table[] に、Person*型,Student*型を混在して保存をした。しかし、Person*として呼び出されると、yama のデータを表示しても、所属・学年は表示されない。上記のプログラムで、所属と名前を表示することはできないのだろうか?

// 混在したPersonを表示
for( int i = 0 ; i < 3 ; i++ )
   table[i]->print() ;
// Student は、所属と名前を表示して欲しい
t-saitoh 55
yamada 21
- ES 1
nomura 22
- PS 2

上記のプログラムでは、Person型では、後でStudent型と区別ができないと困るので、Person型に、Person型(=0)なのか、Student型(=1)なのか区別するための type という型の識別番号を追加し、type=1ならば、Student型として扱うようにしてみた。

// 基底クラス
class Person {
private:
   int  type ; // 型識別情報
   char name[ 20 ] ;
   int  age ;
public:
   Person( int tp , const char s[] , int x )
     : type( tp ) , age( x ) {
      strcpy( name , s ) ;
   }
   int type_person() { return type ; }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( int tp , const char s[] , int x ,
            const char d[] , int g )
            : Person( tp , s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( 0 , "t-saitoh" , 55 ) ;
   Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      switch( table[i]->type_person() ) {
      case 0 :
         table[i]->print() ;
         break ;
      case 1 :
         // 強制的にStudent*型として print() を呼び出す。
         //   最近のC++なら、(static_cast<Student*>(table[i]))->>print() ;
         ((Student*)table[i])->print() ;
         break ;
      }
   }
   return 0 ;
}

しかし、このプログラムでは、プログラマーがこのデータは、Personなので type=0 で初期化とか、Studentなので type=1 で初期化といったことを記述する必要がある。

また、関数を呼び出す際に、型情報(type)に応じて、その型にふさわしい処理を呼び出すための switch 文が必要になる。

もし、派生したクラスの種類がいくつもあるのなら、(1)型情報の代入は注意深く書かないとバグの元になるし、(2)型に応じた分岐処理は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミングでは、巨大な switch 文が問題となっていた。巨大な switch 文は、選択肢だけの if else-if else-if が並ぶと処理効率も悪い。

仮想関数

上記の、型情報の埋め込みと巨大なswitch文の問題の解決策として、C++では仮想関数(Virtual Function)が使える。

型に応じて異なる処理をしたい関数があったら、その関数の前に virtual と書くだけで良い。このような関数を、仮想関数と呼ぶ。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   virtual void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   virtual void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[i]->print() ;
   }
   return 0 ;
}

クラスの中に仮想関数が使われると、C++ では、プログラム上で見えないが、何らかの型情報をオブジェクトの中に保存してくれる。

また、仮想関数が呼び出されると、その型情報を元に、ふさわしい関数を自動的に呼び出してくれる。このため、プログラムも table[i]->print() といった極めて簡単に記述できるようになる。

関数ポインタ

仮想関数の仕組みを実現するためには、関数ポインタが使われる。

以下の例では、返り値=int,引数(int,int)の関数( int(*)(int,int) )へのポインタfpに、最初はaddが代入され、(*fp)(3,4) により、7が求まる。

int add( int a , int b ) {
   return a + b ;
}
int mul( int a , int b ) {
   return a * b ;
}
int main() {
   int (*fp)( int , int ) ;
   fp = add ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7
   fp = mul ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12

   int (*ftable[2])( int , int ) = {
      add , mul ,
   } ;
   for( int i = 0 ; i < 2 ; i++ )
      printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ;
   return 0 ;
}

仮想関数を使うクラスが宣言されると、一般的にそのコンストラクタでは、各クラス毎の仮想関数へのポインタのテーブルが型情報として保存されるのが一般的。仮想関数の呼び出しでは、仮想関数へのポインタを使って処理を呼び出す。このため効率よく仮想関数を動かすことができる。

仮想関数の実装方法

仮想関数の一般的な実装方法としては、仮想関数を持つオブジェクトには型情報として仮想関数へのポインタテーブルへのポインタを保存する。この場合、仮想関数の呼び出しは、object->table[n]( arg… ) のような処理が行われる。

オブジェクト指向プログラミング(ソフトウェア工学)

オブジェクト指向プログラミングは、最近の多くのプログラム言語で取り入れられている機能。

今回は、構造化プログラミングオブジェクト指向(クラス,メソッド)、コンストラクタ派生継承仮想関数の概念を紹介する。

オブジェクト指向プログラミングの歴史

最初のプログラム言語のFortran(科学技術計算向け言語)の頃は、処理を記述するだけだったけど、 COBOL(商用計算向け言語)ができた頃には、データをひとまとめで扱う「構造体」(C言語ならstruct {…}の考えができた。(データの構造化)

// C言語の構造体 // データの構造化
struct Person { // 1人分のデータ構造をPersonとする
   char name[ 20 ] ;             // 名前
   int  b_year, b_month, b_day ; // 誕生日
} ;

一方、初期のFortranでは、プログラムの処理順序は、if 文と goto 文で組み合わせて書くこと多く、処理がわかりにくかった。その後のALGOLの頃には、処理をブロック化して扱うスタイル(C言語なら{ 文 … }の複文で 記述する方法ができてきた。(処理の構造化)

      // ブロックの考えがない時代の雰囲気をC言語で表すと
      int i = 0 ;
LOOP: if ( i >= 10 ) goto EXIT ;
         if ( i % 2 != 0 ) goto NEXT ;
            printf( "%d " , i ) ;
NEXT:    i++ ;
      goto LOOP ;   // 処理の範囲を字下げ(インデント)で強調
EXIT:
--------------------------------------------------- 
      // C 言語で書けば
      int i ;
      for( i = 0 ; i < 10 ; i++ ) { // 処理の構造化
         if ( i % 2 == 0 ) {
            printf( "%d¥n" , i ) ;
         }
      }
---------------------------------------------------
      ! 構造化文法のFORTRANで書くと
      integer i
      do i = 0 , 9
        if ( mod( i , 2 ) == 0 ) then
          print * , i
        end if
      end do

このデータの構造化・処理の構造化により、プログラムの分かりやすさは向上し、このデータと処理をブロック化した書き方は「構造化プログラミング(Structured Programming)」 と呼ばれる。

この後、様々なプログラム言語が開発され、C言語などもできてきた。 一方で、シミュレーションのプログラム開発(例simula)では、 シミュレーション対象(object)に対して、命令するスタイルの書き方が生まれ、 データに対して命令するという点で、擬人法のようなイメージで直感的にも分かりやすかった。 これがオブジェクト指向プログラミング(Object Oriented Programming)の始まりとなる。略記するときは OOP などと書くことが多い。

この考え方を導入した言語の1つが Smalltalk であり、この環境では、プログラムのエディタも Smalltalk で記述したりして、オブジェクト指向がGUIのプログラムと親和性が良いことから、この考え方は多くのプログラム言語へと取り入れられていく。

C言語にこのオブジェクト指向を取り入れ C++ が開発される。さらに、この文法をベースとした Java などが開発されている。最近の新しい言語では、どれもオブジェクト指向の考えが使われている。

クラスの導入(構造体でオブジェクト指向もどき)

例えば、名前と年齢の構造体で処理を記述する場合、 以下の様な記載を行うことで、データ設計者データ利用者で分けて 仕事ができる。

// この部分はデータ構造の設計者が書く
// データ構造を記述
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¥n" , p->name , p->age ) ;
}
// -------------------------------------------
// この部分は、データ利用者が書く
int main() {
   // Personの中身を知らなくてもいいから配列を定義(データ隠蔽)
   struct Person saitoh ;
   setPerson( &saitoh , "saitoh" , 55 ) ;

   struct Person table[ 10 ] ; // 初期化は記述を省略
   for( int i = 0 ; i < 10 ; i++ ) {
      // 出力する...という雰囲気で書ける(手続き隠蔽)
      printPerson( &table[i] ) ;
   }
   return 0 ;
}

このプログラムの書き方では、mainの中を読むだけもで、 データ初期化とデータ出力を行うことはある程度理解できる。 この時、データ構造の中身を知らなくてもプログラムが理解でき、 データ実装者はプログラムを記述できる。これをデータ構造の隠蔽化という。 一方、setPerson()や、printPerson()という関数の中身についても、 初期化・出力の方法をどうするのか知らなくても、 関数名から動作は推測できプログラムも書ける。 これを手続きの隠蔽化という。

C++のクラスで表現

上記のプログラムをそのままC++に書き直すと以下のようになる。特徴として 構造体を進化させた class 宣言の中に、データ構造とデータ構造を使う関数をまとめて記述する。

#include <stdio.h>
#include <string.h>

// この部分はクラス設計者が書く
class Person {
private: // ■ クラス外からアクセスできない部分
   // データ構造を記述
   char name[10] ; // メンバーの宣言
   int  age ;
public: // ■ クラス外から使える部分
   // データに対する処理を記述
   void set( char s[] , int a ) { // ■ メソッドの宣言
      // pのように対象のオブジェクトを明記する必要はない。
      strcpy( name , s ) ;
      age = a ;
   }
   void print() {
      printf( "%s %d¥n" , name , age ) ;
   }
} ; // ← 注意ここのセミコロンを書き忘れないこと。

// この部分はクラス利用者が書く
int main() {
   Person saitoh ;
   saitoh.set( "saitoh" , 55 ) ;
   saitoh.print() ;

   // 文法エラーの例
   printf( "%d¥n" , saitoh.age ) ; // ■ age は private なので参照できない。
   return 0 ;
}

用語の解説:C++のプログラムでは、データ構造データの処理を、並行しながら記述する。 データ構造に対する処理は、メソッド(method)と呼ばれる。 データ構造とメソッドを同時に記載したものは、クラス(class)と呼ぶ。 そのclassに対し、具体的な値や記憶域が割り当てられたものをオブジェクト(object)とかインスタンス(instance)と呼ぶ。

コンストラクタ

データ構造を扱ううえで、データの初期化や廃棄処理は重要となるが、書き忘れをすることも多い。そこで、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¥n" , name , age ) ;
   }
} ;
int main() {
   Person saitoh( "saitoh" , 57 ) ;  // ■ コンストラクタで宣言&初期化
   return 0 ;
}

派生と継承

オブジェクト指向では、元となったデータ構造(class ? struct ?)を拡張した時の記述が便利。例えば、前述の Person に住所も覚えたかったとしたら どう書くだろうか?

// オブジェクト指向を使わずに記述
// Personを要素として持つ PersonAddr を定義
struct PersonAddr {
   struct Person person ;
   char          addr[ 20 ] ;
} ;
// PersonAddr のメソッド
void setPersonAddr( struct PersonAddr* p , char nm[] , int ag , char ad[] ) {
   setPerson( p->person , nm , ag ) ;
   strcpy( p->addr , ad ) ;
}
void printPersonAddr( struct PersonAddr* p ) {
   printPerson( p->person ) ;
   printf( "%s" , p->addr ) ;
   return 0 ;
}

オブジェクト指向では、こういった場合には、Person を拡張した PersonAddr を定義する。この時、元となるクラス(Person)は基底クラス(あるいは親クラス)拡張したクラス(PersonAddr)は派生クラス(あるいは子クラス)と呼ぶ。

// C++ 流に記述
class PersonAddr : public Person { // ■ Personから派生したPersonAddr
private:        // ~~~~~~~~~~~~~派生
   char  addr[ 20 ] ;
public:
   PersonAddr( char nm[] , int ag , char ad[] )
     : Person( nm , ag ) {         // ■ 基底クラスのコンストラクタで初期化
       strcpy( addr , ad ) ;       // ■ 追加部分の初期化
   }
} ;
int main() {
   Person     tohru( "tohru" , 57 ) ;
   PersonAddr saitoh( "saitoh" , 45 , "Fukui" ) ;
   tohru.print() ;   // tohru 57
   saitoh.print() ;  // saitoh 45 ■ 継承を使って表示
   return 0 ;
}

この例では、saitoh は PersonAddr であり、それを表示するための PersonAddr::print() は定義されていないが、saitoh.print() を実行すると、基底クラスのメソッド Person::print() を使ってくれる。このように基底クラスのメソッドを流用してくれる機能継承と呼ぶ。

仮想関数

前述の継承のプログラムでは、PersonAddr 専用の print() を定義してもいい。また基底クラスと派生クラスが混在する配列を作ることもできる。しかし、以下のようなプログラムでは問題が発生する。

class PersonAddr : public Person {
private:
   char  addr[ 20 ] ;
public:
   PersonAddr( char nm[] , int ag , char ad[] )
     : Person( nm , ag ) {
       strcpy( addr , ad ) ;
   }
   void print() {  // ■ PersonAddr 専用の print() を宣言してもいい
      Person::print() ;  // 基底クラス Person の print() を使う
      printf( "%s" , addr ) ;
   }
} ;
int main() {
   Person     tohru( "tohru" , 57 ) ;
   PersonAddr saitoh( "saitoh" , 45 , "Fukui" ) ;

   Person*    family[] = {  // tohru と saitoh のポインタ配列
      &tohru , &saitoh      // &saitoh は Person* に降格されている
   } ;
   tohru.print() ;   // tohru 57
   saitoh.print() ;  // saitoh 45 Fukui
   for( int i = 0 ; i < 2 ; i++ )  // tohru 57
      family[ i ]->print() ;       // ■ "saitoh 45"としか表示してくれない

   return 0 ;
}

family[] のデータを全部表示したいのなら、”tohru 57, saitoh 45 Fukui” と表示されてほしい。
こういった場合に、データのclassに応じて適切なメソッドを呼び出すメカニズムとして仮想関数がある。

class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( char nm[] , int ag ) {
      strcpy( name , nm ) ;
      age = ag ;
   }
   virtual void print() {  // ■ ここに virtual が追加された|仮想関数
      printf( "%s %d" , name , age ) ;
   }
} ;
class PersonAddr : public Person {
private:
   char  addr[ 20 ] ;
public:
   PersonAddr( char nm[] , int ag , char ad[] )
     : Person( nm , ag ) {
       strcpy( addr , ad ) ;
   }
   virtual void print() { // ■ ここに virtual が追加された|仮想関数
      Person::print() ;
      printf( "%s" , addr ) ;
   }
} ;
int main() {
   Person     tohru( "tohru" , 57 ) ;
   PersonAddr saitoh( "saitoh" , 45 , "Fukui" ) ;

   Person*    family[] = {  // tohru と saitoh のポインタ配列
      &tohru , &saitoh
   } ;
   for( int i = 0 ; i < 2 ; i++ )  // tohru 57
      family[ i ]->print() ;       // ■ saitoh 45 Fukui と表示
                                   // print は 仮想関数がそれぞれ定義してあるので
                                   // tohru は、"tohru 57"と表示されるし
   return 0 ;                      // saitoh は、"saitoh 45 Fukui"と表示される。
}

このプログラムでは、Person::print() と PersonAddr::print() がそれぞれ仮想関数で定義されているので、tohru, saitoh の各インスタンスには、型情報が埋め込まれている。このため family[0]->print() では “tohru 57” が表示されるし、family[1]->print() では “saitoh 45 Fukui” が表示される。

多態性・ポリモーフィズム

このように、派生クラスと仮想関数使ってプログラムを書くと、その派生クラスに応じた処理を呼び出すことができる。このように共通の基底クラスから様々な派生クラスを作りながらプログラムを書き、その派生クラス毎にそのデータに応じた処理を実行させることができる。インスタンスがデータ種別に応じた動きをすることは多態性(ポリモーフィズム)と呼ばれる。

抽象クラス(純粋仮想基底クラス)

前回説明した仮想関数では、基底クラスから派生させたクラスを作り、そのデータが混在してもクラスに応じた関数(仮想関数)を呼び出すことができる。

この仮想関数の機能を逆手にとったプログラムの記述方法として、抽象クラス(純粋仮想基底クラス)がある。その使い方を説明する。

JavaのGUIにおける派生の使い方

先週の講義では、派生を使ったプログラミングは、GUI で使われていることを紹介したが、例として Java のプログラミングスタイルを少し紹介する。

例えば、Java で アプレット(ブラウザの中で動かすJavaプログラム)を作る場合の、最小のサンプルプログラムは、以下のようになる。

import java.applet.Applet; // C言語でいうところの、Applet 関連の処理を include
import java.awt.Graphics;

public class App1 extends Applet {  // Applet クラスから、App1 クラスを派生
    public void paint(Graphics g) { // 画面にApp1を表示する必要がある時に呼び出される。
        g.drawString("Hello World." , 100 , 100);
    }
}

この例では、ブラウザのGUIを管理する Applet クラスから、App1 というクラスを派生(extendsキーワード)し、App1 固有の処理として、paint() メソッドを定義している。GUI のプログラミングでは、本来ならマウスが押された場合の処理などを記述する必要があるが、このプログラムでは paint() 以外何も書かれていない。これはマウス処理などは、基底クラスのAppletのマウス処理が継承されるので、省略してもうまく動くようになっている。

純粋仮想基底クラス

純粋仮想基底クラスとは、見かけ上はデータを何も持たないクラスであり、本来なら意味がないデータ構造となってしまう。しかし、派生クラスで要素となるデータと仮想関数で機能を与えることで、基底クラスという共通部分から便利な活用ができる。(実際には、型を区別するための型情報を持っている)

例えば、C言語であれば一つの配列に、整数、文字列、実数といった異なる型のデータを記憶させることは本来ならできない。しかし、以下のような処理を記載すれば、可能となる。

C言語では、1つの記憶域を共有するために共用体(union)を使うが、C++では仮想関数が使えるようになり、型の管理をプログラマーが行う必要のある「面倒で危険な」共用体を使う必要はなくなった。

// 純粋仮想基底クラス
class Object {
public:
   virtual void print() const = 0 ;
   // 中身の無い純粋基底クラスで、
   // 仮想関数を記述しない時の書き方。
} ;

// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%d\n" , data ) ;
   }
} ;

// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() const {
      printf( "%s\n" , data ) ;
   }
} ;

// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() const {
      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++ ) { // 123
      data[i]->print() ;            // abc
   }                                // 1.23 と表示
   return 0 ;
} ;

このプログラムでは、純粋仮想基底クラスObjectから、整数IntObject, 文字列StringObject, 実数DoubleObject を派生させ、そのデータを new により生成し、Objectの配列に保存している。

仮想関数を使うと、Object型の中に自動的に型情報が保存されるようになる。一般的な実装では、各派生クラス用の仮想関数のポインタテーブル(vtable)へのポインタが使われる。

Javaなどのオブジェクト指向言語では、全てのクラス階層のスーパークラスとして、Object を持つように作られている。

様々な型に適用できるプログラム

次に、純粋仮想基底クラスの特徴を応用したプログラムの作り方を説明する。

例えば、以下のような最大選択法で配列を並び替えるプログラムがあったとする。

int a[5] = { 11, 55, 22, 44, 33 } ;

void my_sort( int array[] , int size ) {
   for( int i = 0 ; i < size - 1 ; i++ ) {
      int max = i ;
      for( int j = i + 1 ; j < size ; j++ ) {
         if ( array[j] > array[max] )
            max = j ;
      }
      int tmp = array[i] ;
      array[i] = array[max] ;
      array[max] = tmp ;
   }
}
int main() {
   my_sort( a , 5 ) ;
}

しかし、この整数を並び替えるプログラムがあっても、文字列の並び替えや、実数の並び替えがしたい場合には、改めて文字列用並び替えの関数を作らなければいけない。しかも、ほとんどが同じような処理で、改めて指定された型のためのプログラムを作るのは面倒である。

C言語のデータの並び替えを行う、qsort() では、関数ポインタを用いることで、様々なデータの並び替えができる。しかし、1件あたりのデータサイズや、データ実体へのポインタを正しく理解する必要がある。

#include <stdio.h>
#include <stdlib.h>
int a[ 4 ] = { 11, 33, 22, 44 } ;
double b[ 3 ] = { 1.23 , 5.55 , 0.11 } ;
// 並び替えを行いたいデータ専用の比較関数を作る。
// a>bなら+1, a=bなら0, a<bなら-1を返す関数
int cmp_int( int* pa , int* pb ) { // int型用コールバック関数
   return *pa - *pb ;
}
int cmp_double( double* pa , double* pb ) { // double型用コールバック関数
   if ( *pa == *pb )
      return 0 ;
   else if ( *pa > *pb )
      return 1 ;
   else
      return -1 ;
}
int main() {                                   // C言語の怖さ
   qsort( a , 4 , sizeof( int ) ,              //   このあたりの引数を書き間違えると
          (int(*)(void*,void*)) cmp_int ) ;    //   とんでもない目にあう。
   qsort( b , 3 , sizeof( double ) ,
          (int(*)(void*,void*)) cmp_double ) ;
} 

このように、自分が作っておいた関数のポインタを、関数に渡して呼び出してもらう方法は、コールバックと呼ぶ。
JavaScript などの言語では、こういったコールバックを使ったコーディングがよく使われる。

// コールバック関数 f を呼び出す関数
function exec_callback( var f ) {
   f() ;
}
// コールバックされる関数 foo()
function foo() {
   console.log( "foo()" ) ;
}
// foo() を実行する。
exec_callback( foo ) ;
// 無名関数を実行する。
exec_callback( function() {
                  console.log( "anonymous" ) ;
               } ) ;

任意のデータを並び替え

class Object {
public:
   virtual void print() const = 0 ;
   virtual int  cmp( Object* ) = 0 ;
} ;

// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%d\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      int pdata = ((IntObject*)p)->data ;  // 本当はこのキャストが危険
      return data - pdata ;                //  ↓安全な実装したいなら↓
   }                                       // IntObject* pi = dynamic_cast<IntObject*>(p) ;
} ;                                        // return pi != NULL ? data - pi->data : 0 ;

// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() const {
      printf( "%s\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      char* pdata = ((StringObject*)p)->data ;
      return strcmp( data , pdata ) ; // 文字列比較関数
   }
} ;

// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%lf\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      double pdata = ((DoubleObject*)p)->data ;
      if ( data == pdata )
         return 0 ;
      else if ( data > pdata )
         return 1 ;
      else
         return -1 ;
   }
} ;

// Objectからの派生クラスでcmp()メソッドを
//   持ってさえいれば、どんな型でもソートができる。
void my_sort( Object* array[] , int size ) {
   for( int i = 0 ; i < size - 1 ; i++ ) {
      int max = i ;
      for( int j = i + 1 ; j < size ; j++ ) {
         if ( array[j]->cmp( array[max] ) > 0 )
            max = j ;
      }
      Object* tmp = array[i] ;
      array[i] = array[max] ;
      array[max] = tmp ;
   }
}
// 動作確認
int main() {
   Object* idata[3] = {
      new IntObject( 11 ) ,
      new IntObject( 33 ) ,
      new IntObject( 22 ) ,
   } ;
   Object* sdata[3] = {
      new StringObject( "abc" ) ,
      new StringObject( "defghi" ) ,
      new StringObject( "c" ) ,
   } ;
   my_sort( idata , 3 ) ; // 整数のソート
   for( int i = 0 ; i < 3 ; i++ )
      idata[i]->print() ;
   my_sort( sdata , 3 ) ; // 文字列のソート
   for( int i = 0 ; i < 3 ; i++ )
      sdata[i]->print() ;
   return 0 ;
} ;

このような方式でプログラムを作っておけば、新しいデータ構造がでてきてもソートのプログラムを作らなくても、比較専用の関数 cmp() を書くだけで良い。

ただし、この並び替えの例では、Object* を IntObject* に強制的に型変換している。

また、このプログラムでは、データを保管するために new でポインタを保管し、データの比較をするために仮想関数の呼び出しを行うことから、メモリの使用効率も処理効率でもあまりよくない。

こういう場合、最近の C++ ではテンプレート機能が使われる。

template <typename T>
void my_sort( T a[] , int size ) {
  for( int i = 0 ; i < size - 1 ; i++ ) {
    int max = i ;
    for( int j = i + 1 ; j < size ; j++ ) { if ( a[j] > a[max] )
        max = j ;
    }
    T  tmp = a[i] ;
    a[i] = a[max] ;
    a[max] = tmp ;
  }
}

int main() {
  int idata[ 5 ] = { 3, 4, 5 , 1 , 2 } ;
  double fdata[ 4 ] = { 1.23 , 0.1 , 3.4 , 5.6 } ;

  // typename T = int で int::mysort() が作られる
  my_sort<int>( idata , 5 ) ;
  for( int i = 0 ; i < 5 ; i++ )
    printf( "%d " , idata[i] ) ;
  printf( "\n" ) ;

  // typename T = double で double::mysort() が作られる
  my_sort<double>( fdata , 4 ) ;
  for( int i = 0 ; i < 4 ; i++ )
    printf( "%lf " , fdata[i] ) ;
  printf( "\n" ) ;
  return 0 ;
}

C++のテンプレート機能は、my_sort( int[] , int ) で呼び出されると、typename T = int で、整数型用の my_sort() の処理が自動的に作られる。同じく、my_sort( double[] , int ) で呼び出されると、typename = double で 実数型用の my_sort() が作られる。

テンプレート機能では、各型用のコードが自動的に複数生成されるという意味では、出来上がったコードがコンパクトという訳ではない。

仮想関数レポート課題

ここで示したプログラムを参考に、独自のデータ(例えば、複素数のデータや名前と誕生日といったデータ)について、my_sort() などで並び替えるプログラムを作成せよ。並び替える時の順序も、各自て定義すればいい。(複素数なら絶対値順とか、名前と誕生日なら、誕生日順とか)

派生と継承と仮想関数

前回の派生と継承のイメージを改めて記載する。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ; 
   saitoh.print() ; // 表示 t-saitoh 55
   yama.print() ;   // 表示 yamada 21
                    //      - ES 1
   nomu.print() ;   // 表示 nomura 22
   return 0 ;       //      - PS 2
}

このような処理でのデータ構造は、次のようなイメージで表される。

派生クラスでの問題提起

基底クラスのオブジェクトと、派生クラスのオブジェクトを混在してプログラムを記述したらどうなるであろうか?
上記の例では、Person オブジェクトと、Student オブジェクトがあったが、それをひとまとめで扱いたいこともある。

以下の処理では、Person型の saitoh と、Student 型の yama, nomu を、一つの table[] にまとめている。

int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[ i ]->print() ;
   }
   return 0 ;
}

C++では、Personへのポインタの配列に代入する時、Student型ポインタは、その基底クラスへのポインタとしても扱える。ただし、このように記述すると、table[] には、Person クラスのデータして扱われる。

このため、このプログラムを動かすと、以下のように、名前と年齢だけが3人分表示される。

t-saitoh 55
yamada   21
nomura   22

派生した型に応じた処理

上記のプログラムでは、 Person* table[] に、Person*型,Student*型を混在して保存をした。しかし、Person*として呼び出されると、yama のデータを表示しても、所属・学年は表示されない。上記のプログラムで、所属と名前を表示することはできないのだろうか?

// 混在したPersonを表示
for( int i = 0 ; i < 3 ; i++ )
   table[i]->print() ;
// Student は、所属と名前を表示して欲しい
t-saitoh 55
yamada 21
- ES 1
nomura 22
- PS 2

上記のプログラムでは、Person型では、後でStudent型と区別ができないと困るので、Person型に、Person型(=0)なのか、Student型(=1)なのか区別するための type という型の識別番号を追加し、type=1ならば、Student型として扱うようにしてみた。

// 基底クラス
class Person {
private:
   int  type ; // 型識別情報
   char name[ 20 ] ;
   int  age ;
public:
   Person( int tp , const char s[] , int x )
     : type( tp ) , age( x ) {
      strcpy( name , s ) ;
   }
   int type_person() { return type ; }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( int tp , const char s[] , int x ,
            const char d[] , int g )
            : Person( tp , s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( 0 , "t-saitoh" , 55 ) ;
   Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      switch( table[i]->type_person() ) {
      case 0 :
         table[i]->print() ;
         break ;
      case 1 :
         // 強制的にStudent*型として print() を呼び出す。
         //   最近のC++なら、(static_cast<Student*>(table[i]))->>print() ;
         ((Student*)table[i])->print() ;
         break ;
      }
   }
   return 0 ;
}

しかし、このプログラムでは、プログラマーがこのデータは、Personなので type=0 で初期化とか、Studentなので type=1 で初期化といったことを記述する必要がある。

また、関数を呼び出す際に、型情報(type)に応じて、その型にふさわしい処理を呼び出すための switch 文が必要になる。

もし、派生したクラスの種類がいくつもあるのなら、(1)型情報の代入は注意深く書かないとバグの元になるし、(2)型に応じた分岐処理は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミングでは、巨大な switch 文が問題となっていた。巨大な switch 文は、選択肢だけの if else-if else-if が並ぶと処理効率も悪い。

仮想関数

上記の、型情報の埋め込みと巨大なswitch文の問題の解決策として、C++では仮想関数(Virtual Function)が使える。

型に応じて異なる処理をしたい関数があったら、その関数の前に virtual と書くだけで良い。このような関数を、仮想関数と呼ぶ。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   virtual void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   virtual void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[i]->print() ;
   }
   return 0 ;
}

クラスの中に仮想関数が使われると、C++ では、プログラム上で見えないが、何らかの型情報をオブジェクトの中に保存してくれる。

また、仮想関数が呼び出されると、その型情報を元に、ふさわしい関数を自動的に呼び出してくれる。このため、プログラムも table[i]->print() といった極めて簡単に記述できるようになる。

関数ポインタ

仮想関数の仕組みを実現するためには、関数ポインタが使われる。

以下の例では、返り値=int,引数(int,int)の関数( int(*)(int,int) )へのポインタfpに、最初はaddが代入され、(*fp)(3,4) により、7が求まる。

int add( int a , int b ) {
   return a + b ;
}
int mul( int a , int b ) {
   return a * b ;
}
int main() {
   int (*fp)( int , int ) ;
   fp = add ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7
   fp = mul ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12

   int (*ftable[2])( int , int ) = {
      add , mul ,
   } ;
   for( int i = 0 ; i < 2 ; i++ )
      printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ;
   return 0 ;
}

仮想関数を使うクラスが宣言されると、一般的にそのコンストラクタでは、各クラス毎の仮想関数へのポインタのテーブルが型情報として保存されるのが一般的。仮想関数の呼び出しでは、仮想関数へのポインタを使って処理を呼び出す。このため効率よく仮想関数を動かすことができる。

仮想関数の実装方法

仮想関数の一般的な実装方法としては、仮想関数を持つオブジェクトには型情報として仮想関数へのポインタテーブルへのポインタを保存する。この場合、仮想関数の呼び出しは、object->table[n]( arg… ) のような処理が行われる。

抽象クラスの純粋指定

専攻科のオブジェクト指向プログラミングの授業で、抽象クラスの説明をしたけど、抽象クラスで仮想関数が定義できないところでは、以下のような ” = 0 ” で、仮想関数が無いことを明示する必要がある。「んで、=0 ってなんで zero やねん?」との疑問。

ということで、いろいろと試してみた。

class A {
public:
    virtual void print() const = 0 ;
} ;
// この書き方が基本。

class A {
public:
    virtual void print() const = 123 ; // 数字を書いてみた。
} ;
// error: invalid pure specifier (only ‘= 0’ is allowed) before ‘;’ token

‘= 0′ だけが許されているとのエラーだし、これがすべてかな。

でも、なんとなく気に入らないので、C言語では、NULL は、’#define NULL 0′ で定義されているので、’= NULL’ と書けば、仮想関数のポインタが NULL っぽいし、この書き方が、自分的には、一番しっくりくるんだけど…

#include <stdio.h>
class A {
public:
    virtual void print() const = NULL ;
} ;
// error: invalid pure specifier (only ‘= 0’ is allowed) before ‘;’ token

どうも C++ では、’#define NULL 0′ ではないみたい。試しに、先頭に ‘#define NULL 0′ を書いたら、’#define NULL __null’ で定義されていて違うよ…みたいなエラーが表示された。んで、’__null’ って何?とは思うけど、こういうことらしい。ポインタの型のデータ長の’0’で定義されている

ためしに、C++ の null で初期化は…と思ったけど、やっぱりだめ。

class A {
public:
    virtual void print() const = null ;
} ;
// error: invalid pure specifier (only ‘= 0’ is allowed) before ‘;’ token

派生と継承と仮想関数

前回の派生と継承のイメージを改めて記載する。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ; 
   saitoh.print() ; // 表示 t-saitoh 55
   yama.print() ;   // 表示 yamada 21
                    //      - ES 1
   nomu.print() ;   // 表示 nomura 22
   return 0 ;       //      - PS 2
}

このような処理でのデータ構造は、次のようなイメージで表される。

派生クラスでの問題提起

基底クラスのオブジェクトと、派生クラスのオブジェクトを混在してプログラムを記述したらどうなるであろうか?
上記の例では、Person オブジェクトと、Student オブジェクトがあったが、それをひとまとめで扱いたいこともある。

以下の処理では、Person型の saitoh と、Student 型の yama, nomu を、一つの table[] にまとめている。

int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[ i ]->print() ;
   }
   return 0 ;
}

C++では、Personへのポインタの配列に代入する時、Student型ポインタは、その基底クラスへのポインタとしても扱える。ただし、このように記述すると、table[] には、Person クラスのデータして扱われる。

このため、このプログラムを動かすと、以下のように、名前と年齢だけが3人分表示される。

t-saitoh 55
yamada   21
nomura   22

派生した型に応じた処理

上記のプログラムでは、 Person* table[] に、Person*型,Student*型を混在して保存をした。しかし、Person*として呼び出されると、yama のデータを表示しても、所属・学年は表示されない。上記のプログラムで、所属と名前を表示することはできないのだろうか?

// 混在したPersonを表示
for( int i = 0 ; i < 3 ; i++ )
   table[i]->print() ;
// Student は、所属と名前を表示して欲しい
t-saitoh 55
yamada 21
- ES 1
nomura 22
- PS 2

上記のプログラムでは、Person型では、後でStudent型と区別ができないと困るので、Person型に、Person型(=0)なのか、Student型(=1)なのか区別するための type という型の識別番号を追加し、type=1ならば、Student型として扱うようにしてみた。

// 基底クラス
class Person {
private:
   int  type ; // 型識別情報
   char name[ 20 ] ;
   int  age ;
public:
   Person( int tp , const char s[] , int x )
     : type( tp ) , age( x ) {
      strcpy( name , s ) ;
   }
   int type_person() { return type ; }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( int tp , const char s[] , int x ,
            const char d[] , int g )
            : Person( tp , s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( 0 , "t-saitoh" , 55 ) ;
   Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      switch( table[i]->type_person() ) {
      case 0 :
         table[i]->print() ;
         break ;
      case 1 :
         // 強制的にStudent*型として print() を呼び出す。
         //   最近のC++なら、(static_cast<Student*>(table[i]))->>print() ;
         ((Student*)table[i])->print() ;
         break ;
      }
   }
   return 0 ;
}

しかし、このプログラムでは、プログラマーがこのデータは、Personなので type=0 で初期化とか、Studentなので type=1 で初期化といったことを記述する必要がある。

また、関数を呼び出す際に、型情報(type)に応じて、その型にふさわしい処理を呼び出すための switch 文が必要になる。

もし、派生したクラスの種類がいくつもあるのなら、(1)型情報の代入は注意深く書かないとバグの元になるし、(2)型に応じた分岐処理は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミングでは、巨大な switch 文が問題となっていた。

仮想関数

上記の、型情報の埋め込みと巨大なswitch文の問題の解決策として、C++では仮想関数(Virtual Function)が使える。

型に応じて異なる処理をしたい関数があったら、その関数の前に virtual と書くだけで良い。このような関数を、仮想関数と呼ぶ。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   virtual void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   virtual void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[i]->print() ;
   }
   return 0 ;
}

クラスの中に仮想関数が使われると、C++ では、プログラム上で見えないが、何らかの型情報をオブジェクトの中に保存してくれる。

また、仮想関数が呼び出されると、その型情報を元に、ふさわしい関数を自動的に呼び出してくれる。このため、プログラムも table[i]->print() といった極めて簡単に記述できるようになる。

関数ポインタ

仮想関数の仕組みを実現するためには、関数ポインタが使われる。

以下の例では、返り値=int,引数(int,int)の関数( int(*)(int,int) )へのポインタfpに、最初はaddが代入され、(*fp)(3,4) により、7が求まる。

int add( int a , int b ) {
   return a + b ;
}
int mul( int a , int b ) {
   return a * b ;
}
int main() {
   int (*fp)( int , int ) ;
   fp = add ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7
   fp = mul ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12

   int (*ftable[2])( int , int ) = {
      add , mul ,
   } ;
   for( int i = 0 ; i < 2 ; i++ )
      printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ;
   return 0 ;
}

仮想関数を使うクラスが宣言されると、一般的にそのコンストラクタでは、各クラス毎の仮想関数へのポインタのテーブルが型情報として保存されるのが一般的。仮想関数の呼び出しでは、仮想関数へのポインタを使って処理を呼び出す。このため効率よく仮想関数を動かすことができる。

仮想関数の実装方法

仮想関数の一般的な実装方法としては、仮想関数を持つオブジェクトには型情報として仮想関数へのポインタテーブルへのポインタを保存する。

純粋仮想基底クラス

前回説明した仮想関数では、基底クラスから派生させたクラスを作り、そのデータが混在してもクラスに応じた関数(仮想関数)を呼び出すことができる。

この仮想関数の機能を逆手にとったプログラムの記述方法として、純粋仮想基底クラスがある。その使い方を説明する。

JavaのGUIにおける派生の使い方

先週の講義では、派生を使ったプログラミングは、GUI で使われていることを紹介したが、例として Java のプログラミングスタイルを少し紹介する。

例えば、Java で アプレット(ブラウザの中で動かすJavaプログラム)を作る場合の、最小のサンプルプログラムは、以下のようになる。

import java.applet.Applet; // C言語でいうところの、Applet 関連の処理を include
import java.awt.Graphics;

public class App1 extends Applet {  // Applet クラスから、App1 クラスを派生
    public void paint(Graphics g) { // 画面にApp1を表示する必要がある時に呼び出される。
        g.drawString("Hello World." , 100 , 100);
    }
}

この例では、ブラウザのGUIを管理する Applet クラスから、App1 というクラスを派生(extendsキーワード)し、App1 固有の処理として、paint() メソッドを定義している。GUI のプログラミングでは、本来ならマウスが押された場合の処理などを記述する必要があるが、このプログラムでは paint() 以外何も書かれていない。これはマウス処理などは、基底クラスのAppletのマウス処理が継承されるので、省略してもうまく動くようになっている。

純粋仮想基底クラス

純粋仮想基底クラスとは、見かけ上はデータを何も持たないクラスであり、本来なら意味がないデータ構造となってしまう。しかし、派生クラスで要素となるデータと仮想関数で機能を与えることで、基底クラスという共通部分から便利な活用ができる。(実際には、型を区別するための型情報を持っている)

例えば、C言語であれば一つの配列に、整数、文字列、実数といった異なる型のデータを記憶させることは本来ならできない。しかし、以下のような処理を記載すれば、可能となる。

C言語では、1つの記憶域を共有するために共用体(union)を使うが、C++では仮想関数が使えるようになり、型の管理をプログラマーが行う必要のある「面倒で危険な」共用体を使う必要はなくなった。

// 純粋仮想基底クラス
class Object {
public:
   virtual void print() const = 0 ;
   // 中身の無い純粋基底クラスで、
   // 仮想関数を記述しない時の書き方。
} ;

// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%d\n" , data ) ;
   }
} ;

// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() const {
      printf( "%s\n" , data ) ;
   }
} ;

// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() const {
      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++ ) { // 123
      data[i]->print() ;            // abc
   }                                // 1.23 と表示
   return 0 ;
} ;

このプログラムでは、純粋仮想基底クラスObjectから、整数IntObject, 文字列StringObject, 実数DoubleObject を派生させ、そのデータを new により生成し、Objectの配列に保存している。

仮想関数を使うと、Object型の中に自動的に型情報が保存されるようになる。一般的な実装では、各派生クラス用の仮想関数のポインタテーブル(vtable)へのポインタが使われる。

Javaなどのオブジェクト指向言語では、全てのクラス階層のスーパークラスとして、Object を持つように作られている。

様々な型に適用できるプログラム

次に、純粋仮想基底クラスの特徴を応用したプログラムの作り方を説明する。

例えば、以下のような最大選択法で配列を並び替えるプログラムがあったとする。

int a[5] = { 11, 55, 22, 44, 33 } ;

void my_sort( int array[] , int size ) {
   for( int i = 0 ; i < size - 1 ; i++ ) {
      int max = i ;
      for( int j = i + 1 ; j < size ; j++ ) {
         if ( array[j] > array[max] )
            max = j ;
      }
      int tmp = array[i] ;
      array[i] = array[max] ;
      array[max] = tmp ;
   }
}
int main() {
   my_sort( a , 5 ) ;
}

しかし、この整数を並び替えるプログラムがあっても、文字列の並び替えや、実数の並び替えがしたい場合には、改めて文字列用並び替えの関数を作らなければいけない。しかも、ほとんどが同じような処理で、改めて指定された型のためのプログラムを作るのは面倒である。

C言語のデータの並び替えを行う、qsort() では、関数ポインタを用いることで、様々なデータの並び替えができる。しかし、1件あたりのデータサイズや、データ実体へのポインタを正しく理解する必要がある。

#include <stdio.h>
#include <stdlib.h>
int a[ 4 ] = { 11, 33, 22, 44 } ;
double b[ 3 ] = { 1.23 , 5.55 , 0.11 } ;
// 並び替えを行いたいデータ専用の比較関数を作る。
// a>bなら+1, a=bなら0, a<bなら-1を返す関数
int cmp_int( int* pa , int* pb ) { // int型用コールバック関数
   return *pa - *pb ;
}
int cmp_double( double* pa , double* pb ) { // double型用コールバック関数
   if ( *pa == *pb )
      return 0 ;
   else if ( *pa > *pb )
      return 1 ;
   else
      return -1 ;
}
int main() {                                   // C言語の怖さ
   qsort( a , 4 , sizeof( int ) ,              //   このあたりの引数を書き間違えると
          (int(*)(void*,void*)) cmp_int ) ;    //   とんでもない目にあう。
   qsort( b , 3 , sizeof( double ) ,
          (int(*)(void*,void*)) cmp_double ) ;
} 

このように、自分が作っておいた関数のポインタを、関数に渡して呼び出してもらう方法は、コールバックと呼ぶ。
JavaScript などの言語では、こういったコールバックを使ったコーディングがよく使われる。

// コールバック関数 f を呼び出す関数
function exec_callback( var f ) {
   f() ;
}
// コールバックされる関数 foo()
function foo() {
   console.log( "foo()" ) ;
}
// foo() を実行する。
exec_callback( foo ) ;
// 無名関数を実行する。
exec_callback( function() {
                  console.log( "anonymous" ) ;
               } ) ;

任意のデータを並び替え

class Object {
public:
   virtual void print() const = 0 ;
   virtual int  cmp( Object* ) = 0 ;
} ;

// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%d\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      int pdata = ((IntObject*)p)->data ;  // 本当はこのキャストが危険
      return data - pdata ;                //  ↓安全な実装したいなら↓
   }                                       // IntObject* pi = dynamic_cast<IntObject*>(p) ;
} ;                                        // return pi != NULL ? data - pi->data : 0 ;

// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() const {
      printf( "%s\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      char* pdata = ((StringObject*)p)->data ;
      return strcmp( data , pdata ) ; // 文字列比較関数
   }
} ;

// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%lf\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      double pdata = ((DoubleObject*)p)->data ;
      if ( data == pdata )
         return 0 ;
      else if ( data > pdata )
         return 1 ;
      else
         return -1 ;
   }
} ;

// Objectからの派生クラスでcmp()メソッドを
//   持ってさえいれば、どんな型でもソートができる。
void my_sort( Object* array[] , int size ) {
   for( int i = 0 ; i < size - 1 ; i++ ) {
      int max = i ;
      for( int j = i + 1 ; j < size ; j++ ) {
         if ( array[j]->cmp( array[max] ) > 0 )
            max = j ;
      }
      Object* tmp = array[i] ;
      array[i] = array[max] ;
      array[max] = tmp ;
   }
}
// 動作確認
int main() {
   Object* idata[3] = {
      new IntObject( 11 ) ,
      new IntObject( 33 ) ,
      new IntObject( 22 ) ,
   } ;
   Object* sdata[3] = {
      new StringObject( "abc" ) ,
      new StringObject( "defghi" ) ,
      new StringObject( "c" ) ,
   } ;
   my_sort( idata , 3 ) ; // 整数のソート
   for( int i = 0 ; i < 3 ; i++ )
      idata[i]->print() ;
   my_sort( sdata , 3 ) ; // 文字列のソート
   for( int i = 0 ; i < 3 ; i++ )
      sdata[i]->print() ;
   return 0 ;
} ;

このような方式でプログラムを作っておけば、新しいデータ構造がでてきてもソートのプログラムを作らなくても、比較専用の関数 cmp() を書くだけで良い。

ただし、この並び替えの例では、Object* を IntObject* に強制的に型変換している。

また、このプログラムでは、データを保管するために new でポインタを保管し、データの比較をするために仮想関数の呼び出しを行うことから、メモリの使用効率も処理効率でもあまりよくない。

こういう場合、最近の C++ ではテンプレート機能が使われる。

template <typename T>
void my_sort( T a[] , int size ) {
  for( int i = 0 ; i < size - 1 ; i++ ) {
    int max = i ;
    for( int j = i + 1 ; j < size ; j++ ) { if ( a[j] > a[max] )
        max = j ;
    }
    T  tmp = a[i] ;
    a[i] = a[max] ;
    a[max] = tmp ;
  }
}

int main() {
  int idata[ 5 ] = { 3, 4, 5 , 1 , 2 } ;
  double fdata[ 4 ] = { 1.23 , 0.1 , 3.4 , 5.6 } ;

  // typename T = int で int::mysort() が作られる
  my_sort( idata , 5 ) ;
  for( int i = 0 ; i < 5 ; i++ )
    printf( "%d " , idata[i] ) ;
  printf( "\n" ) ;

  // typename T = double で double::mysort() が作られる
  my_sort( fdata , 4 ) ;
  for( int i = 0 ; i < 4 ; i++ )
    printf( "%lf " , fdata[i] ) ;
  printf( "\n" ) ;
  return 0 ;
}

C++のテンプレート機能は、my_sort( int[] , int ) で呼び出されると、typename T = int で、整数型用の my_sort() の処理が自動的に作られる。同じく、my_sort( double[] , int ) で呼び出されると、typename = double で 実数型用の my_sort() が作られる。

テンプレート機能では、各型用のコードが自動的に複数生成されるという意味では、出来上がったコードがコンパクトという訳ではない。

愚痴

ここにあるサンプルコードを試していたけど、以下のような間違いしたけど、エラ〜メッセージが「抽象クラスのオブジェクトとは作れない(error: allocating an object of abstract class type ‘BA’)」というのは、間違い探すのに一苦労だった。C++の文法が変更されたかと思ったぜ。

class A {
public:
   virtual void print() = 0 ; // const のつけ忘れ
} ;                           // print() const = 0 ;
class BA : public A {
private: int b ;
public:  virtual void print() const {
            printf( "%d" , b ) ;
         }
} ;
int main() {
   BA ba ;
}

派生と継承と仮想関数

前回の派生と継承のイメージを改めて記載する。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ; 
   saitoh.print() ; // 表示 t-saitoh 55
   yama.print() ;   // 表示 yamada 21
                    //      - ES 1
   nomu.print() ;   // 表示 nomura 22
   return 0 ;       //      - PS 2
}

このような処理でのデータ構造は、次のようなイメージで表される。

派生クラスでの問題提起

基底クラスのオブジェクトと、派生クラスのオブジェクトを混在してプログラムを記述したらどうなるであろうか?
上記の例では、Person オブジェクトと、Student オブジェクトがあったが、それをひとまとめで扱いたいこともある。

以下の処理では、Person型の saitoh と、Student 型の yama, nomu を、一つの table[] にまとめている。

int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[ i ]->print() ;
   }
   return 0 ;
}

C++では、Personへのポインタの配列に代入する時、Student型ポインタは、その基底クラスへのポインタとしても扱える。ただし、このように記述すると、table[] には、Person クラスのデータして扱われる。

このため、このプログラムを動かすと、以下のように、名前と年齢だけが3人分表示される。

t-saitoh 55
yamada   21
nomura   22

派生した型に応じた処理

上記のプログラムでは、 Person* table[] に、Person*型,Student*型を混在して保存をした。しかし、Person*として呼び出されると、yama のデータを表示しても、所属・学年は表示されない。上記のプログラムで、所属と名前を表示することはできないのだろうか?

// 混在したPersonを表示
for( int i = 0 ; i < 3 ; i++ )
   table[i]->print() ;
// Student は、所属と名前を表示して欲しい
t-saitoh 55
yamada 21
- ES 1
nomura 22
- PS 2

上記のプログラムでは、Person型では、後でStudent型と区別ができないと困るので、Person型に、Person型(=0)なのか、Student型(=1)なのか区別するための type という要素を追加し、type=1ならば、Student型として扱うようにしてみた。

// 基底クラス
class Person {
private:
   int  type ; // 型識別情報
   char name[ 20 ] ;
   int  age ;
public:
   Person( int tp , const char s[] , int x )
     : type( tp ) , age( x ) {
      strcpy( name , s ) ;
   }
   int type_person() { return type ; }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( int tp , const char s[] , int x ,
            const char d[] , int g )
            : Person( tp , s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( 0 , "t-saitoh" , 55 ) ;
   Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      switch( table[i]->type_person() ) {
      case 0 :
         table[i]->print() ;
         break ;
      case 1 :
         // 強制的にStudent*型として print() を呼び出す。
         //   最近のC++なら、(static_cast<Student*>(table[i]))->>print() ;
         ((Student*)table[i])->print() ;
         break ;
      }
   }
   return 0 ;
}

しかし、このプログラムでは、プログラマーがこのデータは、Personなので type=0 で初期化とか、Studentなので type=1 で初期化といったことを記述する必要がある。また、型情報(type)に応じて、その型にふさわしい処理を呼び出すための switch 文が必要になる。

もし、派生したクラスの種類がいくつもあるのなら、型情報の代入は注意深く書かないとバグの元になるし、型に応じた分岐は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミングでは、巨大な switch 文が問題となっていた。

仮想関数

上記の、型情報の埋め込みと巨大なswitch文の問題の解決策として、C++では仮想関数(Virtual Function)が使える。

型に応じて異なる処理をしたい関数があったら、その関数の前に virtual と書くだけで良い。このような関数を、仮想関数と呼ぶ。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   virtual void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   virtual void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[i]->print() ;
   }
   return 0 ;
}

クラスの中に仮想関数が使われると、C++ では、プログラム上で見えないが、何らかの型情報をオブジェクトの中に保存してくれる。

また、仮想関数が呼び出されると、その型情報を元に、ふさわしい関数を自動的に呼び出してくれる。このため、プログラムも table[i]->print() といった極めて簡単に記述できるようになる。

関数ポインタ

仮想関数の仕組みを実現するためには、関数ポインタが使われる。

以下の例では、返り値=int,引数(int,int)の関数( int(*)(int,int) )へのポインタfpに、最初はaddが代入され、(*fp)(3,4) により、7が求まる。

int add( int a , int b ) {
   return a + b ;
}
int mul( int a , int b ) {
   return a * b ;
}
int main() {
   int (*fp)( int , int ) ;
   fp = add ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7
   fp = mul ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12

   int (*ftable[2])( int , int ) = {
      add , mul ,
   } ;
   for( int i = 0 ; i < 2 ; i++ )
      printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ;
   return 0 ;
}

仮想関数を使うクラスが宣言されると、一般的にそのコンストラクタでは、各クラス毎の仮想関数へのポインタのテーブルが型情報として保存されるのが一般的。仮想関数の呼び出しでは、仮想関数へのポインタを使って処理を呼び出す。このため効率よく仮想関数を動かすことができる。

派生と継承

前回の派生と継承のイメージを改めて記載する。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
void main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ; 
   saitoh.print() ; // 表示 t-saitoh 55
   yama.print() ;   // 表示 yamada 21
                    //      - ES 1
   nomu.print() ;   // 表示 nomura 22
}                   //      - PS 2

このような処理でのデータ構造は、次のようなイメージで表される。

派生クラスでの問題提起

基底クラスのオブジェクトと、派生クラスのオブジェクトを混在してプログラムを記述したらどうなるであろうか?
上記の例では、Person オブジェクトと、Student オブジェクトがあったが、それをひとまとめで扱いたいこともある。

以下の処理では、Person型の saitoh と、Student 型の yama, nomu を、一つの table[] にまとめている。

void main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[ i ]->print() ;
   }
}

C++では、Personへのポインタの配列に代入する時、Student型ポインタは、その基底クラスへのポインタとしても扱える。ただし、このように記述すると、table[] には、Person クラスのデータして扱われる。

このため、このプログラムを動かすと、以下のように、名前と年齢だけが3人分表示される。

t-saitoh 55
yamada   21
nomura   22

派生した型に応じた処理

上記のプログラムでは、 Person* table[] に、Person*型,Student*型を混在して保存をした。しかし、Person*として呼び出されると、yama のデータを表示しても、所属・学年は表示されない。上記のプログラムで、所属と名前を表示することはできないのだろうか?

// 混在したPersonを表示
for( int i = 0 ; i < 3 ; i++ )
   table[i]->print() ;
// Student は、所属と名前を表示して欲しい
t-saitoh 55
yamada 21
- ES 1
nomura 22
- PS 2

上記のプログラムでは、Person型では、後でStudent型と区別ができないと困るので、Person型に、Person型(=0)なのか、Student型(=1)なのか区別するための type という要素を追加し、type=1ならば、Student型として扱うようにしてみた。

// 基底クラス
class Person {
private:
   int  type ; // 型識別情報
   char name[ 20 ] ;
   int  age ;
public:
   Person( int tp , const char s[] , int x )
     : type( tp ) , age( x ) {
      strcpy( name , s ) ;
   }
   int type_person() { return type ; }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( int tp , const char s[] , int x ,
            const char d[] , int g )
            : Person( tp , s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
void main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( 0 , "t-saitoh" , 55 ) ;
   Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      switch( table[i]->type_person() ) {
      case 0 :
         table[i]->print() ;
         break ;
      case 1 :
         // 強制的にStudent*型として print() を呼び出す。
         //   最近のC++なら、(static_cast<Student*>(table[i]))->>print() ;
         ((Student*)table[i])->print() ;
         break ;
      }
   }
}

しかし、このプログラムでは、プログラマーがこのデータは、Personなので type=0 で初期化とか、Studentなので type=1 で初期化といったことを記述する必要がある。また、型情報(type)に応じて、その型にふさわしい処理を呼び出すための switch 文が必要になる。

もし、派生したクラスの種類がいくつもあるのなら、型情報の代入は注意深く書かないとバグの元になるし、型に応じた分岐は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミングでは、巨大な switch 文が問題となっていた。

仮想関数

上記の、型情報の埋め込みと巨大なswitch文の問題の解決策として、C++では仮想関数(Virtual Function)が使える。

型に応じて異なる処理をしたい関数があったら、その関数の前に virtual と書くだけで良い。このような関数を、仮想関数と呼ぶ。

// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   virtual void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   virtual void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
void main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[i]->print() ;
   }
}

クラスの中に仮想関数が使われると、C++ では、プログラム上で見えないが、何らかの型情報をオブジェクトの中に保存してくれる。

また、仮想関数が呼び出されると、その型情報を元に、ふさわしい関数を自動的に呼び出してくれる。このため、プログラムも table[i]->print() といった極めて簡単に記述できるようになる。

関数ポインタ

仮想関数の仕組みを実現するためには、関数ポインタが使われる。

以下の例では、返り値=int,引数(int,int)の関数( int(*)(int,int) )へのポインタfpに、最初はaddが代入され、(*fp)(3,4) により、7が求まる。

int add( int a , int b ) {
   return a + b ;
}
int mul( int a , int b ) {
   return a * b ;
}
void main() {
   int (*fp)( int , int ) ;
   fp = add ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7
   fp = mul ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12

   int (*ftable[2])( int , int ) = {
      add , mul ,
   } ;
   for( int i = 0 ; i < 2 ; i++ )
      printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ;
}

仮想関数を使うクラスが宣言されると、一般的にそのコンストラクタでは、各クラス毎の仮想関数へのポインタのテーブルが型情報として保存されるのが一般的。仮想関数の呼び出しでは、仮想関数へのポインタを使って処理を呼び出す。このため効率よく仮想関数を動かすことができる。

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

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

課題で取り組んでいるプログラムは、純粋仮想基底クラス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() を使うのだろうか?

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

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー