抽象クラスと仮想関数


純粋仮想関数と抽象クラス

仮想関数を使うことにより、基本クラスのポインタを管理するだけで派生クラスに応じた処理を実行させることが出来ることを学びました。基本クラスに定義した仮想関数を共通の呼びだし口(インターフェース)にして、多様な派生クラス群を動作させることができます。

基本クラスの目的がインターフェースを決めることだけで、それ自体のインスタンスは不必要になる場合があります。

次のような例を考えてみましょう。犬を表すDog クラスと猫を表す Cat クラスを作成し、それぞれに鳴く(音を立てる)というメソッド Sound を作成して管理するとしましょう。一つの方法は以下のようなコードになります。

// Soundという仮想メンバ関数を持った Dogクラスを定義
class Dog {
public:
    virtual void Sound() { cout << "わほわほ" << endl; }  // 犬はわほわほ
};


// Dogの派生クラスとしてCatクラスを定義
// Sound 仮想関数をオーバーライドします
class Cat : public Dog {
public:
     virtual void Sound() { cout << "みゃーみゃー" << endl; }  // 猫はみゃーみゃー
};

このように定義した DogクラスとCatクラスのインスタンスは、基本クラスであるDogクラスへのポインタで管理できます。

void main()
{
    Dog* dogs[4];// Dogクラスのポインタの配列         
    
    dogs[0] = new Dog();
    dogs[1] = new Cat();
    dogs[2] = new Dog();
    dogs[3] = new Cat();
    
    for (int i = 0;i < 4;i++)
        dogs[i]->Sound();
}
実行結果

わほわほ
みゃーみゃー
わほわほ
みゃーみゃー

やりたいことはできているのですが、このクラス継承関係にはどうにも気持ち悪さが残りますよね。だいたい犬も猫も Dogクラスのポインタで管理するのはちょっとおかしい。

オブジェクト指向の設計の話しをちょっとだけしますが、一般に継承関係にあるオブジェクトの基本クラスと派生クラスには "is-a" の関係を持つものがふさわしいとされています。Derived is a Base. つまり派生クラスは基本クラスの一種である。という関係ですね。猫は犬の一種では・・・あきらかにありません。

こうした場合の解法は、基本クラスに動物という意味のAnimalクラスを作成し、そこからDogクラスとCatクラスを派生するというものです。犬は動物の一種ですし、猫も動物の一種ですよね。こうすると犬も猫も Animalクラスのポインタで管理することができます。コードを以下に示します。

// Soundという仮想メンバ関数を持った Animalクラスを定義
class Animal {
public:
    virtual void Sound() {   }  // 関数の中身はからっぽ
};  
  
  
  // Animalを派生してDogクラスを定義
class Dog : public Animal{
public:
    virtual void Sound() { cout << "わほわほ" << endl; }  // 犬はわほわほ
};

// Animalを派生してCatクラスを定義
class Cat : public Animal {
public:
     virtual void Sound() { cout << "みゃーみゃー" << endl; }  // 猫はみゃーみゃー
};


void main()
{
    Animal* animals[4];// Animal クラスのポインタの配列         
    
    animals[0] = new Dog();
    animals[1] = new Cat();
    animals[2] = new Dog();
    animals[3] = new Cat();
    
    for (int i = 0;i < 4;i++)
        animals[i]->Sound();
}

ここで、Animalクラスで Soundという仮想関数が定義されていますが、その中身はからっぽです。動物というおおざっぱなくくりでは、この関数を実装することができないのです。つまりはこの Animal クラスは Soundという名前のインターフェースを定義しているだけで、その実装をする必要がないのです。

このようにインターフェースだけを定義して、その実装が出来ないもの/必要ないものを純粋仮想関数と呼び、以下のように定義します。

class Animal {
public:
    virtual void Sound() = 0;  // 純粋仮想関数    
};  
  

純粋仮想関数には実装部はありません。純粋仮想関数をもったクラスを抽象クラスと呼びます。抽象クラスは実態を持たない関数を持っているのでインスタンス化することはできません。派生クラスを定義して、その派生クラスを使うことを前提としたクラスなのです。

(練習問題7)

上のサンプルプログラムに下記の修正を加えなさい。ただし、プロジェクト名は p3k3_7、ソースファイル名は p3k3_7.cppとしなさい。(ヘッダ部を分割する必要はない。)

  1. Animalクラスを抽象クラスに修正して実行しなさい。
  2. Animalクラスを抽象クラスにした後に、Animalクラスのオブジェクトをインスタンス化しようとすると、どのようなエラーが出るかを確認しなさい。(インスタンス化:オブジェクトをメモリ上に配置すること。メイン関数に Animal Dog;  などを加えればよい。)
  3. (好きな動物のクラスを Animalクラスの派生クラスとして作成し、実行しなさい。)