C++マニアック

抽象クラス,Virtual Class,C++言語

このページでは C++ の抽象クラスの使い方について解説します。抽象クラスの厳密な定義はさておき、その心の解説に重点を置くことにします。

  1. 抽象クラスを使う場面
  2. より単純な例
  3. 共通項をくくり出して抽象クラス化する
  4. 他の例
  5. ポインタを使う場合の注意(virtual デストラクタの話)
  6. 抽象クラス関連本

抽象クラスを使う場面 ページの先頭へ

抽象クラスをサクッと理解してもらうために、これを使うとぴったりはまる場面について、まずは考えてみましょう。
一言で言うと、異なる役者に同じ役を演じてもらうということなのですが、例えば、ちょっとリアルに、次のような場合を頭に思い浮かべてみてください。

画像をファイリングするアプリを作成しているとします。
画像を何処かから取ってきては、ファイリングするわけですが、画像の出どころが次のように複数種類あるとします。

  1. Web 上の URL から画像を得る。
  2. ユーザが持ってきた USB メモリに入った画像をユーザの操作で得る。
  3. PC に接続されたカメラで画像を撮影する。

「Web 上の URL から画像を得る」ためには、Web に接続して、URL で示される画像をダウンロードしてくる必要があります。
「USB メモリの画像」の場合には、画像ファイルを選択する窓を出して、ユーザに選択してもらう必要があるでしょう。
「PC に接続されたカメラ」の場合には、カメラをオープンし、プレビュー画面を表示して、ユーザに撮影ボタンを押してもらう必要があるでしょう。

このような場合、通常、上記それぞれの画像の出どころに対応するクラスを作成します。クラスを役者とみなすと、3 人の役者がいるということですね。それぞれの役者は、初期化の方法とか、操作の方法は異なるわけですが、役柄は同じで、1 枚の画像を得ることです。

抽象クラスは、この「1 枚の画像を得る」という共通部分を規格化するためのものと言っていいでしょう。

ナオ: 「クラス=役者」ですか?
店主: そういうことだね。
ナオ: 「役柄=メソッド」ですか?
店主: 話が早いね。

より単純な例 ページの先頭へ

上記は、リアリティを出すために画像のファイリングを例としましたが、抽象クラスの解説にはもっと単純な方が良いので、次は、画像ではなく、整数を得るという超簡単な例を頭に思い浮かべてみてください。整数を得る方法も 2 種類に限定しておきます。

  1. 整数の種を元にしてランダムな整数を得る。
  2. ユーザが入力した整数を得る。

全く別の方法で、同じ整数(=数値は問わない)を得るということが重要なポイントです。
それでは、上記用のクラスを定義してみましょう。これらは、まだ、抽象クラスを使用しない普通のクラスです。

// 整数の種を元にしてランダムな整数を得る。
class CRandomNumber {
public:
    CRandomNumber(unsigned int uiSeed) { srand(uiSeed); }
    int GetNumber() { return rand(); }
};
// ユーザが入力した整数を得る。
class CNumberFromUser {
public:
    int GetNumber() {
        char acBuffer[1000];
        gets(acBuffer);
        return atoi(acBuffer);
    }
};

上記二つのクラスには GetNumber() という同じ名前のメソッド(=関数)が定義されていることに注目しておいてください。
上記クラスを使用するコードは、例えば次のようになります。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 上記クラス定義をここに置く。
int main() {
    // オブジェクトを作成する。
    CRandomNumber   randomnumber(time(0));
    CNumberFromUser numberfromuser;
    // メソッドを呼び出す。
    int iNumber0 = randomnumber  .GetNumber(); // ランダムな数値が発生する。
    int iNumber1 = numberfromuser.GetNumber(); // ユーザが数値を入力する。
    // 結果を表示してみる。
    printf("iNumber0 = %d\n", iNumber0);
    printf("iNumber1 = %d\n", iNumber1);
    return 0;
}

共通項をくくり出して抽象クラス化する ページの先頭へ

上記例の main() 関数では、CRandomNumber と CNumberFromUser の合計 2 個のオブジェクトの GetNumber() を個別に呼び出して整数値をプリントしており、これはこれで「めでたしめでたし」ではあるのですが、このような処理を行う部分を一つの関数とかクラスにまとめたいという要望が浮かんで来る場合があります。
例えば、最初の画像を扱う例などで言えば、画像を得た後、「画像処理をして、通し番号をつけてファイリングする」というような、ある程度まとまった処理を行うのが普通なので、その処理を関数にまとめたいと思うわけですね。
そこで、ここでは、「整数を得てプリント」するという部分を関数化することを考えてみましょう。
そのような関数は、CRandomNumber または CNumberFromUser 型のオブジェクトの参照、あるいは、ポインタをもらって、関数内部で GetNumber() を呼び出すことになるでしょう。
例えば、CRandomNumber 用の関数を作成してみると、次のようになります。

void PrintNumber(CRandomNumber& rrandomnumber) {
    printf("iNumber = %d\n", rrandomnumber.GetNumber());
}

これは、これでよいのですが、上記関数は CRandomNumber 型専用で、CNumberFromUser には使えません。
PrintNumber() は CRandomNumber と CNumberFromUser 両用にしたいわけです。
ここで、抽象クラスの出番となります。
どうするかというと、CRandomNumber と CNumberFromUser の共通項をくくり出した形の抽象クラスを次のように定義し、両クラスをこれから派生した形に書き換えます。

class CNumberBase {
public:
    virtual int GetNumber() = 0;
};

上記では、GetNumber()virtual を付け仮想関数であることを示しています。また、関数本体を定義しないで = 0 としています。このようにすると、CNumberBase は、抽象クラスになります。抽象クラスは、これから派生するクラスのベースクラスとしてしか使用できず、それ自身のオブジェクトを new することは出来ません。

これに対し関数本体を普通に定義しても良く、その場合、オブジェクトを new で実体化することができ、この場合、抽象クラスとは呼ばないことになります。
クラス内に一つでも純粋仮想関数があると、そのクラスは抽象クラスであるといいます。

さて、上記抽象クラスを使用して、元の 2 個のクラスを次のように修正します。

// 整数の種を元にしてランダムな整数を得る。
class CRandomNumber : public CNumberBase {
public:
    CRandomNumber(unsigned int uiSeed) { srand(uiSeed); }
    /*virtual*/ int GetNumber() { return rand(); }
};
// ユーザが入力した整数を得る。
class CNumberFromUser : public CNumberBase {
public:
    /*virtual*/ int GetNumber() {
        char acBuffer[1000];
        gets(acBuffer);
        return atoi(acBuffer);
    }
};

それぞれのクラスのベースクラスとして、CNumberBase を追加したということですね。/*virtual*/ は、コメントなので、必ずしも追加する必要はありませんが、追加しておいたほうが、対応する仮想関数がベースクラスに存在することが類推しやすいでしょう。
これで、次のようなCNumberBase 派生クラスに共通のプリント関数が書けるようになります。

// CNumberBase 派生クラスに共通のプリント関数
void PrintNumber(CNumberBase& rnumberbase) {
    printf("iNumber = %d\n", rnumberbase.GetNumber());
}

上記を使って、次のようなプログラムが書けます。

int main() {
    // オブジェクトを作成する。
    CRandomNumber   randomnumber(time(0));
    CNumberFromUser numberfromuser;
    // メソッドを呼び出す。
    PrintNumber(randomnumber);     // 共通プリント関数を呼び出す。
    PrintNumber(numberfromuser);   // 共通プリント関数を呼び出す。
    return 0;
}

二つの PrintNumber() には、異なるクラスのオブジェクトが渡されていることに注目してください。
共通プリント関数 PrintNumber() の中で呼ばれている rnumberbase.GetNumber() は、ベースクラス CNumberBase の GetNumber() が仮想関数であるため、それぞれの派生クラスの GetNumber() を実際には呼び出します。

これは、CNumberBase に定義されている GetNumber() に関数本体があってもそのようになります。CNumberBase 型のオブジェクトを直接生成して PrintNumber() に渡せば CNumberBase の GetNumber() が実行されます。

PrintNumber() には、CRandomNumber と CNumberFromUser という異なる型の役者が渡されてくるわけですが、役者は常に同じ「整数を得る」という役を演じるわけですね。
上記は、抽象クラス CNumberBase を使って、それから派生するクラスに対して GetNumber() というメソッドを持つように規格化を行ったというふうに見ることができます。

ナオ: 抽象クラスに共通のメソッドをくくり出して、機能を規格化するってことですね。
店主: そういうこと。
ナオ: そうすると、ベースクラスオブジェクトへのポインタや参照経由で、派生クラスのメソッドが使えてしまうということですね。
店主: そう。ポインタや参照が指し示すオブジェクトの実際の型によって、呼び出されるメソッドが決まるということなのだよ。

他の例 ページの先頭へ

抽象クラスをベースクラスとして、その virtual な関数を「同じ役」=「共通項」を定義するために使用する、ということの意味を更に解説するために、もう一つの例を挙げましょう。

今回は、「開く」という性能を持った「扉」を表すベースクラスを定義してみます。単純化のため「閉じる」機能は、省略します。

class CDoor {
public:
    virtual void Open() = 0;  // 扉を開くという共通項
};

扉は左右に開くとして、左の扉と、右の扉をそれぞれ上記 CDoor から派生させると、次のように定義できます。

class CLeftDoor : public CDoor {
public:
    /*virtual*/ void Open() { /* ここで、左扉をに動かす */ }
};
class CRightDoor : public CDoor {
public:
    /*virtual*/ void Open() { /* ここで、右扉をに動かす */ }
};

注目すべきなのは、同じ Open() という名前の関数が、Open() では扉を左に動かすのに対して、Open() では、逆の、右に動かすということです。つまり、同じ Open() という名前で、「開く」という意味を表す関数ではありますが、各派生クラスの Open() で行われることの内容は異なっており、具体的な動きは同じではないということです。

つまり、「同じ役」=「共通項」の意味は、プログラマの意図としては、関数名 Open() に集約されおり、実際の動作は各派生クラス毎にその意味を実現すべく個別の動作をするということですね。

上記のように抽象クラスの virtual な関数は、通常その名前に意味を込める事になるので、「仮想ベースクラスで機能を規格化し、派生クラスを自分以外の人間が作成する」というような場合には特に、基本クラスの関数名を、「共通項」の意味を適切に表すような名前にすることがとても大事だと言えます。これは、「自分以外の人間」「未来や過去の自分」である場合にも言えることです。この名前付けをおろそかにすると、後で他ならぬ自分が窮することになりやすいので気をつけましょう。

ナオ: 「未来の自分」は「自分以外の人間」なのでしょうか?
店主: ...それにつきましては、まだ決定してございません。
ナオ: では、「過去の自分」は「自分以外の人間」なのでしょうか?
店主: ...それは、記憶にございません。

To be continued ...