異なるクラス(型)のインスタンス(実態、変数)を統一して扱えると便利なときがあります。例えば、文字や数字は異なる型ですが、表示するという動作自体はそれほど変わりません。
次の例でそのことを説明します。上の文を読んでみて用語を忘れてしまったことに気づいた人はここで復習してください。
プロジェクト名:virtual1
この例では、int, char, 文字列(char型のポインタ)をメンバ変数(プロパティ)に持つクラスをそれぞれInt, Char, Textとして定義しています。main関数では各クラスのインスタンスのメンバに値を代入しています。そして、最後に表示しています。
実行すると以下の様になります。
次に下のプログラムの様に各クラスに表示のためのメンバ関数を定義します。
main関数の、表示の部分を見てください。どのインスタンスもprint()を実行することで表示可能です。
ここではインスタンスは3つなのでそれほど問題ないですが、例えば、
num1.i = 100; num2.i=101;num3.i=102 (...中略...) num100.i = 200;
ch1.c = 'A'; ch2.c = 'B'; (...中略...) ch25.c='Y'; ch26.c = 'Z';
t1.t = "t1"; (...中略 ...) t100.t = "t100";
と、上のように多数のインスタンスがあると表示するのが大変です。表示するためだけに同じような文を200行以上書かないといけません。何とか繰り返し文を使えないでしょうか。
ここで問題になるのは、3つの異なる型があるということです。今までの知識では、型ごとに繰り返し文を書くことが出来ます。例えば各クラスでインスタンスを配列にして、以下のように書けます。
Int num[100];
Char ch[26];
Text t[100];
num[0].i = 100;(..中略 ..)
ch[0].c ='A'; (..中略 ...)
t[0].t ="t1";(...中略 ...)
for(int i = 0; i < 100; i++){ num[i].print();}
for(int j = 0; j < 26; j++){ ch[j].printf();}
for(int k = 0; k < 100; k++){ t[k].printf();}
しかし、ここでやりたいのは、一つの繰り返し文で全てを表示することです。次のプログラムを見てください。
プロジェクト名:virtual2
このプログラで重要なことは、Int, Char, Textが全てObjectから派生していることです。Objectから派生したクラスのインスタンスは、Object型のポインタでそのアドレスを格納することが出来ます。main関数の
Object* o[3];
o[0] = #
o[1] = &ch;
o[2] = &t;
がその部分です。これで、異なる型を同じ配列にまとめることが出来ました。o[0], o[1], o[2]は単に各オブジェクトのアドレスを格納しているに過ぎないことに注意してください。
これで、一つの繰り返し文で表示できそうですが、問題が残っています。実際にコンパイルしてみると分かりますが、main関数の
o[i]->print();
でエラーになります。これは、o[i]がObject型だからです。Object型にはprint()が定義されていませんので無いメンバ関数を使おうとしたためにエラーが出たのです。それでは、Object型にprint()を定義すればよいということになります。
class Object
{
public:
print(){ /*ここに何を書けばいいのか? */}
};
しかし、この関数では何を書けばいいでしょうか?ここで整理すると、
そして、その答えが以下のプログラムです。
プロジェクト名: virtual2を変更する
このプログラムは、コンパイルが成功し期待通りの実行結果になります。前のプログラムとの違いはObjectの定義だけです。
Objectの定義を見てください。printの前にvirtualを付けると、mainのo[i]->print(); のところで、代入されているオブジェクトのprintが実行されるようになります。
また、
virtual void print() = 0;
の= 0は、Objectから派生したクラスは必ずメンバ関数printを定義しないといけないという意味を表します。Objectのprint関数の様にvirtualと=0がついたメンバ関数を純粋仮想関数(pure virtual function)といいます。
純粋仮想関数を持つクラスは実体化できません。つまり、下の2つの例では、
Object o1; //エラー
Object* o2; //OK。ポインタはよい
o1のようにインスタンス化できません。printの内容がないことからも、納得がいくと思います。Objectは派生して使うクラスなのです。
純粋仮想関数というのがあるので、純粋でない仮想関数があるのかというと、あります。
例えば、新しいクラスRealをObjectから派生させるとします。
以下のクラスをプロジェクト:virtual2のソースのクラスTextの定義の後に挿入してください。
class Real : public Object
{
public:
double r;
};
このクラスは、このままではエラーになります。printの定義が無いからです。次に、Objectの定義を変えます。
class Object
{
public:
virtual void print()
{
printf("表示関数が定義されていません\n");
}
};
この場合、上のクラスRealは正しくコンパイルできます。 このObjectのprintの様にvirtualがついたメンバ関数を仮想関数と呼びます。
Real r;
Int i;
r.r = 10.31;
i.i = 10;
Object* o1 = &r;
Object* o2 = &i;
o1->print();
o2->print();
とすると、何が表示されるでしょうか?分からない人は実際に実行してください。
ここで用語の説明をします。RealとObjectの関係を表す言葉として、RealはObjectから派生したサブクラスといいます。逆に、ObjectはRealに対してスーパークラスといいます。
上の例では、Objectは概念的に何でもあらわします。それに対してRealは実数をあらわすのでObjectよりもRealに当てはまるものは少なくなります。逆にRealに当てはまるものは必ずObjectにも当てはまります。よってRealはObjectのサブクラスであるという言い方をします。
スーパークラスで仮想関数が定義されていると、スーパークラスから派生したサブクラスに関数が定義されていない場合、スーパークラスの仮想関数が使われます。もし、Objectのメンバ関数printにvirtualがなければ上のクラスRealの例ではコンパイルエラーになります。
ところで、クラスObjectは、メンバ変数が一つも無いクラスです。このクラスは派生するクラスが持つべきメンバ関数を指定するためのクラスとも言えます。このようなクラスのことをインターフェイスと呼び、通常のクラスと区別するときがあります。
同じインターフェイスから派生したクラスは内容が異なっても同じように操作できます。
それでは課題をやりましょう。