C++言語解説:2-3.継承と仮想関数
2002-07-22

[概要] オブジェクト指向プログラミングの重要な機能です。継承の方法とアクセス制御、
    多重継承、仮想基本クラス、更に仮想関数を使用した多態性の実現方法までを学
    習します。
    
[構成]・継承
     * 基本クラスと派生クラス
   ・アクセス制御
     * publicとprivate
     * protected
   ・多重継承
     * 多重継承とは
     * スコープ解決演算子
     * 仮想基本クラス
   ・コンストラクタとデストラクタ
     * コンストラクタとデストラクタ実行順位
     * 多重継承時のコンストラクタとデストラクタ実行順位
     * 基本クラスコンストラクタへの仮引数
   ・基本クラスのポインタ
     * 基本クラスポインタの性質
   ・仮想関数と抽象クラス
     * 仮想関数
     * 仮想関数と継承
     * 抽象クラス


継承

 基本クラスと派生クラス
 
  ・C++ではあるクラスを作成する際、他のクラスの属性や操作を受け継いで、合成する
   ことができます
。この機能を継承(inheritance)と呼びます。
  
  ・継承の元になったクラスを基本クラス(base class)、継承を受けたクラスを派生ク
   ラス(derived class)
と呼びます。派生されたクラスは、別のクラスの基本クラスと
   なることができます。この結果多重の階層を持つクラスが出来上がります。この構
   造はクラス階層構造(class hierarchy)と呼ばれます。
   
  ・例として、カウンタをベースにしたラップカウンタを作成してみます。
  
   [List1.継承の例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Counter {
      protected:  //後述します
       int mi_count;
      public:
       void Init(int i_ini=0) {
         mi_count = i_ini;
         return;
       }
       void CountUp(){
         mi_count++;
         return;
       }
       void CountDown(){
         mi_count--;
         return;
       }
       int GetVal(){
         return mi_count;
       }
    };
    
    class LapCounter : public Counter {
       int mi_lapstart;
       int mi_lapend;
      public:
       void LapStart(){
         mi_lapstart = mi_count;
         return;
       }
       void LapEnd(){
         mi_lapend = mi_count;
         return;
       }
       int GetLap(){
         return mi_lapend - mi_lapstart;
       }
    };
    
    int main()
    {
      LapCounter counter_a;
      
      counter_a.Init();
      counter_a.CountUp();  //count 1
      counter_a.CountUp();  //count 2
      counter_a.LapStart();
      counter_a.CountUp();  //count 3 : lap 1
      counter_a.CountUp();  //count 4 : lap 2
      counter_a.LapEnd();
      counter_a.CountUp();  //count 5
      
      cout << counter_a.GetVal() << endl;
      cout << counter_a.GetLap() << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     5
     2
   └───────────────────────────────────┘
  
  ・List1の例では、通常のカウンタ動作を行うCounterクラスをベースとして、ラップ
   カウント機能を持たせたLapCounterクラスを派生しています。継承は以下の形式で
   記述します。
     class 派生クラス名 : アクセス指定子 基本クラス名 {・・・};
  
  ・このように継承を使用すれば、基本のカウンタをベースにした様々なカウンタにつ
   いて差分機能を用意するだけで作成することができます。基本となるCounterクラス
   自体に不具合があった場合も、それを修正するだけで全ての派生クラスが修正され
   ます。


アクセス制御

 publicとprivate
 
  ・アクセス制御はクラスのメンバだけでなく、基本クラスにも適用されます。アクセ
   ス指定子には
     public
     private
     protected
   の3種類があります。protectedについては後述します。
  
  ・publicで継承を行うと、基本クラスのpublicメンバは派生クラスにおいてもpublic
   メンバとして扱われます。
派生クラスから基本クラスのprivateメンバへアクセスす
   ることはできません。
  
  ・privateで継承を行うと、基本クラスのpublicメンバはprivateとして継承されます。
   これにより、派生クラスから基本クラスのpublicメンバにアクセスできても、外部
   からは基本クラスのpublicメンバへアクセスできません。

   [Table1.publicとprivate継承]
   ┌───────────────────────────────────┐
                 ┌───────────┐
                 │   アクセス先   │
    ┌──────┬─────┼─────┬─────┤
    │      │     │基本クラス│基本クラス│
    │ 継承方法 │アクセス元│ public │ private │
    ├──────┼─────┼─────┼─────┤
    │public継承 │派生クラス│  ○  │  ×  │
    │      ├─────┼─────┼─────┤
    │      │ 外部   │  ○  │  ×  │
    ├──────┼─────┼─────┼─────┤
    │private継承 │派生クラス│  ○  │  ×  │
    │      ├─────┼─────┼─────┤
    │      │ 外部   │  ×  │  ×  │
    └──────┴─────┴─────┴─────┘
   └───────────────────────────────────┘


 protected
 
  ・もう一つのアクセス指定子としてprotectedがあります。protectedが単独のクラス
   でメンバへのアクセス指定子に使用されている場合、private指定と同じです。
   protectedは継承されたときの振る舞いに特徴があります。
  
  ・基本クラスがpublicとして継承されると、基本クラスのprotectedメンバは派生クラ
   スでもprotectedメンバとして継承されます。
よって、派生クラスからはアクセスで
   きますが、クラスの外部からはアクセスすることはできません。
  
  ・protectedとして継承されたメンバは更に派生クラスを生成する場合もprotectedと
   して継承
されます。
  
  ・基本クラスがprivateとして継承されると、基本クラスのprotectedメンバはprivate
   となり
、派生クラスからはアクセスできますが、更に1段下の派生クラスからはアク
   セスできくなります。
  
   [Table2.protectedメンバの振るまい(1)]
   ┌───────────────────────────────────┐
                  ┌─────────────────┐
                  │      アクセス先      │
    ┌──────┬──────┼─────┬─────┬─────┤
    │      │      │基本クラス│基本クラス│基本クラス│
    │ 継承方法 │アクセス元 │ public │ private │protected │
    ├──────┼──────┼─────┼─────┼─────┤
    │      │派生クラス1 │  ○  │  ×  │  ○  │
    │      ├──────┼─────┼─────┼─────┤
    │public継承 │派生クラス2 │  ○  │  ×  │  ○  │
    │      ├──────┼─────┼─────┼─────┤
    │      │ 外部    │  ○  │  ×  │  ×  │
    ├──────┼──────┼─────┼─────┼─────┤
    │      │派生クラス1 │  ○  │  ×  │  ○  │
    │      ├──────┼─────┼─────┼─────┤
    │private継承 │派生クラス2 │  ×  │  ×  │  ×  │
    │      ├──────┼─────┼─────┼─────┤
    │      │ 外部    │  ×  │  ×  │  ×  │
    └──────┴──────┴─────┴─────┴─────┘
          基本クラス
            ↑
          派生クラス1  左記の継承を仮定している
            ↑
          派生クラス2
   └───────────────────────────────────┘

  ・基本クラスがprotectedとして継承された場合、基本クラスのpublicメンバと
   protectedメンバは全てprotectedとして継承
されます。そこからの使い分けは上記
   と同様です。


多重継承

 多重継承とは
 
  ・多重継承(multiple inheritance)とは、複数の基本クラスから継承を行うことです。
   例としてList2を見て下さい。
 
   [List2.多重継承の例]
   ┌───────────────────────────────────┐
    // Base1  Base2
    //  ↑   ↑
    //  └─┬─┘
    //  Derievd

    #include <iostream>
    using namespace std;
    
    class Base1 {
      protected:
       int mi_base1;
      public:
       void Input1(int i_tmp){
         mi_base1 = i_tmp;
         return;
       }
    };
    
    class Base2 {
      protected:
       int mi_base2;
      public:
       void Input2(int i_tmp){
         mi_base2 = i_tmp;
         return;
       }
    };
    
    class Derived : public Base1, public Base2 {
      public:
       int Sum(){
         return mi_base1 + mi_base2;
       }
    };
    
    int main()
    {
      Derived obj;
      
      obj.Input1(10);
      obj.Input2(20);
      
      cout << obj.Sum() << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     30
   └───────────────────────────────────┘
  
  ・多重継承を行う場合、カンマ区切りで、各基本クラスにアクセス指定を行います。
   ┌───────────────────────────────────┐
     class 派生クラス : アクセス指定 基本クラス1,
               アクセス指定 基本クラス2,
                    :
               アクセス指定 基本クラスN {・・・};
   └───────────────────────────────────┘
 

 スコープ解決演算子
 
  ・多重継承を行うと、アクセスするメンバが曖昧になってしまうケースが出てきます。
   例えば
   ┌───────────────────────────────────┐
     Base0  Base0
      ↑   ↑
     Base1  Base2
      ↑   ↑
      └─┬─┘
       Derievd
   └───────────────────────────────────┘
   のような多重継承が行われたとします。例をList3に示します。ちなみにList3はコ
   ンパイルエラーになります。
   
   [List3.多重継承が曖昧になる例 (*コンパイルエラーになる)]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base0 {
      public:
       int mi_base0;
    };
    
    class Base1 : public Base0 {
      public:
       int mi_base1;
    };
    
    class Base2 : public Base0 {
      public:
       int mi_base2;
    };

    class Derived : public Base1, public Base2 {
      public:
       int Sum(){
         int i_tmp;
         i_tmp = mi_base1 + mi_base2 + mi_base0;
         return i_tmp;
       }
    };
    
    int main()
    {
      Derived derived;
      
      derived.mi_base0 = 5;
      derived.mi_base1 = 10;
      derived.mi_base2 = 20;
      
      cout << derived.Sum();
      
      return 0;
    }
   └───────────────────────────────────┘
  
  ・List3がコンパイルエラーになる理由ですが、Derivedクラスの中で使用している
     mi_base0
   はベースクラスBase1のものかBase2のものか、どちらを指定しているのかが曖昧に
   なっている
からです。main()関数中の
     derived.mi_base0
   についても同様です。
  
  ・この対処方法としてスコープ解決演算子を利用する方法があります。使用例を
   List4に示します。
   
   [List4.スコープ解決演算子による対応]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base0 {
      :
      : //省略, List3と同様
      :
    class Derived : public Base1, public Base2 {
      public:
       int Sum(){
         int i_tmp;
         i_tmp = mi_base1 + mi_base2 + Base1::mi_base0;
         return i_tmp;
       }
    };
    
    int main()
    {
      Derived derived;
      
      derived.Base1::mi_base0 = 5;
      derived.mi_base1 = 10;
      derived.mi_base2 = 20;
      
      cout << derived.Sum();
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     35
   └───────────────────────────────────┘
  
  ・List4では、Base1側が継承するBase0オブジェクトのmi_base0を指すようにスコープ
   解決演算子(クラス名::)を記述しています。
  
  ・これで曖昧さは無くなったのですが、List4はあくまで例です。実際の場面でクラス
   を利用する側が継承のスコープを意識しなければならなくなった場合、それはクラ
   ス設計についてもう一度分析した方が良い
と言えるでしょう。

   
 仮想基本クラス
 
  ・List4では、Base0クラスの中身をBase1とBase2で共用しても問題有りません。
   ┌───────────────────────────────────┐
       Base0
        ↑
      ┌─┴─┐
     Base1  Base2
      ↑   ↑
      └─┬─┘
       Derievd
   └───────────────────────────────────┘
   このような多重継承を行うには仮想基本クラス(virtual base class)を使用します。
   仮想基本クラスを使用するには、継承時にvirtualキーワードを付けます。
     class 派生クラス : virtual アクセス指定 基本クラス {・・・};
  
  ・List5に、List3を仮想基本クラスで書き換えた例を示します。

   [List5.仮想基本クラスの使用例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base0 {
      public:
       int mi_base0;
    };
    
    class Base1 : virtual public Base0 {
      public:
       int mi_base1;
    };
    
    class Base2 : virtual public Base0 {
      public:
       int mi_base2;
    };

    class Derived : public Base1, public Base2 {
      public:
       int Sum(){
         int i_tmp;
         i_tmp = mi_base1 + mi_base2 + mi_base0;
         return i_tmp;
       }
    };
    
    int main()
    {
      Derived derived;
      
      derived.mi_base0 = 5;
      derived.mi_base1 = 10;
      derived.mi_base2 = 20;
      
      cout << derived.Sum();
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     35
   └───────────────────────────────────┘


コンストラクタとデストラクタ

 コンストラクタとデストラクタ実行順位
 
  ・継承で構成されたクラスのコンストラクタとデストラクタの実行順について考えて
   みます。この場合は例を見る方が早いでしょう。
   ┌───────────────────────────────────┐
      Base0
       ↑
      Derived1
       ↑
      Derived2
   └───────────────────────────────────┘
   の継承クラスにおいて、それぞれのコンストラクタとデストラクタの動きを表示し
   ます。
  
   [List6.派生クラスのコンストラクタとデストラクタ]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base0 {
      public:
       Base0() { cout << "Base0:Constructor\n"; }
       ~Base0() { cout << "Base0:Destructor\n"; }
    };
    
    class Derived1 : public Base0 {
      public:
       Derived1() { cout << "Derived1:Constructor\n"; }
       ~Derived1() { cout << "Derived1:Destructor\n"; }
    };

    class Derived2 : public Derived1 {
      public:
       Derived2() { cout << "Derived2:Constructor\n"; }
       ~Derived2() { cout << "Derived2:Destructor\n"; }
    };
    
    int main()
    {
      Derived2 obj; //コンストラクタとデストラクタだけ実行
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     Base0:Constructor
     Derived1:Constructor
     Derived2:Constructor
     Derived2:Destructor
     Derived1:Destructor
     Base0:Destructor
   └───────────────────────────────────┘
  
  ・この結果からわかるように、コンストラクタは基本クラス側から実行され、デスト
   ラクタは派生クラス側から実行されます。
この理由については、派生クラスは基本
   クラスが存在することを前提に生成されていることを考慮すれば容易に理解できる
   でしょう。


 多重継承時のコンストラクタとデストラクタ実行順位
 
  ・次は多重継承時の実行順を見ることにします。これについても例を見る方が早いで
   しょう。
   ┌───────────────────────────────────┐
     Base0  Base1
      ↑   ↑
      └─┬─┘
       Derievd
   └───────────────────────────────────┘
   上記の多重継承に対応した例をList7に示します。
   
   [List7.多重継承時のコンストラクタとデストラクタ]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base0 {
      public:
       Base0() { cout << "Base0:Constructor\n"; }
       ~Base0() { cout << "Base0:Destructor\n"; }
    };
    
    class Base1 {
      public:
       Base1() { cout << "Base1:Constructor\n"; }
       ~Base1() { cout << "Base1:Destructor\n"; }
    };

    class Derived : public Base0, public Base1 {
      public:
       Derived() { cout << "Derived:Constructor\n"; }
       ~Derived() { cout << "Derived:Destructor\n"; }
    };
    
    int main()
    {
      Derived obj; //コンストラクタとデストラクタだけ実行
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     Base0:Constructor
     Base1:Constructor
     Derived:Constructor
     Derived:Destructor
     Base1:Destructor
     Base0:Destructor
   └───────────────────────────────────┘
   
  ・List7の結果からわかるように、多重継承時はクラスの呼び出し順にコンストラクタ
   が実行されます。またデストラクタはその逆順
で実行されます。


 基本クラスコンストラクタへの仮引数
 
  ・基本クラスのコンストラクタへ仮引数を渡したい場合、派生クラスのコンストラク
   タを以下の書式で記述します。
   ┌───────────────────────────────────┐
     派生クラス名(引数リスト) : 基本クラス名1(引数リスト),
                   基本クラス名2(引数リスト),
                     :
                   基本クラス名N(引数リスト) {・・・}
   └───────────────────────────────────┘
  
  ・基本クラスのコンストラクタへ引数を渡したい場合、派生クラス自身が引数を必要
   としなくても、派生クラス側で引数を受けるように仮引数を設定する必要がありま
   す。List8に例を示します。
   
   [List8.基本クラスのコンストラクタへ仮引数を渡す]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base0 {
      public:
       Base0(int b0) {
         cout << "Base0:" << b0 << endl;
         return;
       }
    };
    
    class Base1 : public Base0 {
      public:
       Base1(int b0, int b1) : Base0(b0) {
         cout << "Base1:" << b1 << endl;
         return;
       }
    };

    class Derived : public Base1 {
      public:
       Derived(int b0, int b1) : Base1(b0, b1) {
         cout << "Derived b0 =" << b0 << endl;
         cout << "Derived b1 =" << b1 << endl;
         return;
       }
    };
    
    int main()
    {
      Derived obj(10,20); //コンストラクタとデストラクタだけ実行
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     Base0:10
     Base1:20
     Derived b0 =10
     Derived b1 =20
   └───────────────────────────────────┘
   

基本クラスのポインタ

 基本クラスポインタの性質
 
  ・継承によりクラスを作成した場合、当然ながら基本クラスと派生クラスがあるわけ
   ですが、このときの基本クラスポインタには特別な性質があります。
  
  ・通常ポインタは自分の型以外のオブジェクトを指すことはできません。しかし、継
   承により作成されたクラスでは基本クラスのポインタで派生クラスのオブジェクト
   を指すことができます。

  
   [List8.基本クラスポインタの性質]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base {
      public:
       void BaseExe() {
         cout << "Base" << endl;
         return;
       }
    };
    
    class Derived : public Base {
      public:
       void DerivedExe() {
         cout << "Derived" << endl;
       }
    };

    int main()
    {
      Base* p_base;   //Base型ポインタ
      Base base;    //Base型オブジェクト
      Derived derived; //Derived型オブジェクト
      
      p_base = &base;   //baseオブジェクトのアドレス
      p_base->BaseExe();
      
      p_base = &derived;  //derivedオブジェクトのアドレス
      p_base->BaseExe();
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     Base
     Base
   └───────────────────────────────────┘
   
  ・基本クラスのポインタで派生クラスオブジェクトを指している場合、基本クラスポ
   インタを介して、基本クラスの要素へアクセスすることはできます
が、派生クラス
   の要素へアクセスすることはできません(*1)。例えば
     p_base->DerivedExe();
   はコンパイルエラーになります。
  
  ・ポインタの演算については、オフセット演算の結果が基本クラスの大きさに基づき
   ます。従って基本クラスポインタが派生クラスのオブジェクトを指している場合、
   演算結果は実質的に無効
となります。
   
  ・参照についても基本クラスの型で受けることができます。List9に例を示します。
  
   [List9.基本クラスによる参照]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base {
     :
     : //BaseとDerivedクラスはList8と同様
     :

    void SubFunc(Base& obj);
    
    int main()
    {
      Derived derived; //Derived型オブジェクト
      
      SubFunc(derived);
      
      return 0;
    }
    
    void SubFunc(Base& obj){
      obj.BaseExe();
      return;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     Base
   └───────────────────────────────────┘
   
  ・ここまでの話では、基本クラスポインタの重要性は見えてこないでしょう。意味を
   理解するには、この後説明する仮想関数の理解が重要です。
   

仮想関数と抽象クラス

 仮想関数
 
  ・仮想関数(virtual function)は基本クラスの関数宣言でvirtualキーワードが付加さ
   れたものです。仮想関数は派生クラスで再定義されます。
     virtual 戻り値型 クラス名::関数名(引数リスト){・・・}
  
  ・先に説明したように、派生クラスオブジェクト内の基本クラス要素には基本クラス
   のポインタを使用してアクセスすることができます。そして基本クラスで定義又は
   宣言した仮想関数は派生クラスで再定義されます。このとき基本クラスポインタで
   仮想関数にアクセスすると、派生クラスで定義した動作になります。

  
  ・仮想関数は1つ以上、すなわち複数の派生クラスで再定義できます。派生クラスで仮
   想関数を再定義する場合、virtualキーワードは不要(付けても良い)です。List10に
   使用例を示します。
  
   [List10.仮想関数の使用例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base {
      public:
       virtual void GetMessage() {
         cout << "This is Base!!" << endl;
         return;
       }
    };
    
    class Derived1 : public Base {
      public:
       void GetMessage() {
         cout << "This is Derived1!!" << endl;
         return;
       }
    };
    
    int main()
    {
      Base* p_base;    //Base型ポインタ
      Base base;     //Base型オブジェクト
      Derived1 derived1; //Derived1型オブジェクト
      
      p_base = &base;
      p_base->GetMessage();
      
      p_base = &derived1;
      p_base->GetMessage();
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     This is Base!!
     This is Derived1!!
   └───────────────────────────────────┘
  
  ・仮想関数による関数の再定義はオーバーライド(overriding)と呼ばれます。仮引数
   の型や数の違いによる関数オーバーロードと異なり、仮想関数の場合は仮引数の数
   と型が完全に一致します。
  
  ・コンストラクタを仮想関数にすることはできませんが、デストラクタは仮想関数と
   することができます
(*2)。派生クラスにより動きが異なる場合、デストラクタに求
   められる後処理もその実装によって異なるからです。
  
  ・仮想関数は基本クラスのポインタからアクセスすることになります。ポインタでア
   クセスするオブジェクトをnew等で動的に確保した場合を考えてみましょう。そうで
   す。仮想関数の呼び出しは実行時に決定されるのです。
  
  ・アクセス先は実行時に決められるので、基本クラスに、インターフェースの役割を
   持つ関数を仮想関数として定義しておけば、実行時の状況に合わせて生成したオブ
   ジェクトに対し、統一したインターフェースでアクセスすることが可能
です。これ
   は使用者から見れば、同じインターフェースが実行状態に合わせて異なる動作をす
   る
ように見えるということです。これはC++で多態性(polymorphism)を実現する土台
   となっています。
  
  ・このため多態性は「1インターフェース複数メソッド」と呼ばれるときもあります。
  
  ・多態性を実現する機能を用いれば、インタフェースを統一して実装部分だけを個別
   に持ったするクラスライブラリの作成と利用が可能になります。例えばBorland系の
   VCLやMicrosoftのMFC等はこの種別のライブラリとなるわけです。


 仮想関数と継承
 
  ・仮想関数は継承されます。基本クラスでvirtualと宣言されれば、階層の数に関係な
   く、その関数はvirtualなままとなります。List11を例に継承階層と呼び出し仮想関
   数の関係を示します。
   
   [List11.仮想関数と継承階層]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base {
      public:
       virtual void GetMessage() {
         cout << "This is Base!!" << endl;
         return;
       }
    };
    
    class Derived1 : public Base {
      public:
       void GetMessage() {
         cout << "This is Derived1!!" << endl;
         return;
       }
    };
    
    class Derived2 : public Derived1 {
      public:
       void GetMessage() {
         cout << "This is Derived2!!" << endl;
         return;
       }
    };
    
    class Derived3 : public Base {
      public:
      void NoExec(){
        return;
      }
    };
    
    int main()
    {
      Base* p_base;    //Base型ポインタ
      Base base;     //Base型オブジェクト
      Derived1 derived1; //Derived1型オブジェクト
      Derived2 derived2; //Derived2型オブジェクト
      Derived3 derived3; //Derived3型オブジェクト
      
      p_base = &base;
      p_base->GetMessage();
      
      p_base = &derived1;
      p_base->GetMessage();
      
      p_base = &derived2;
      p_base->GetMessage();
      
      p_base = &derived3;
      p_base->GetMessage();

      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     This is Base!!
     This is Derived1!!
     This is Derived2!!
     This is Base!!
   └───────────────────────────────────┘
  
  ・Derived2クラスのオブジェクトは
     Base ← Derived1 ← Derived2
   の2階層継承となっています。GetMessage()関数はBaseクラスで仮想関数として宣言
   されており、それは継承でも引き継がれています。よって、Derived2クラスのオブ
   ジェクトに基本クラスポインタでアクセスした場合、GetMessage()はDerived2によ
   ってオーバーライドされたものが動作します。

  
  ・Derived3クラスではGetMessage()が定義されていないため、Baseクラスの
   GetMessage()が呼び出されています。


 抽象クラス
 
  ・常に派生クラスで実装内容が定義されると仮定した関数。これを純粋仮想関数(pure
   virtual function)
と呼びます。基本クラスがインタフェース専用として用意される
   場合、このような状態はごく普通のことです。
   
  ・そして、メンバ関数に純粋仮想関数を含むクラス抽象クラス(abstract class)
   呼びます。
  
  ・純粋仮想関数を含むということは、その関数の内容を定義するために、必ず派生ク
   ラスが必要です。従って抽象クラスは、自身でオブジェクトを定義できませんが、
   多態性実現のためにポインタは定義することができます。

  
  ・純粋仮想関数は
     virtual 戻値型 関数名(引数リスト) = 0;
   と定義(*3)します。List12に例を示します。
   
   [List12.純粋仮想関数の使用例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base {
      public:
       virtual void GetMessage() = 0; //純粋仮想関数
    };
    
    class Derived1 : public Base {
      public:
       virtual void GetMessage() {
         cout << "This is Derived1!!" << endl;
         return;
       }
    };
    
    int main()
    {
      Base* p_base;    //Base型ポインタ
      Derived1 derived1; //Derived1型オブジェクト
      
      p_base = &derived1;
      p_base->GetMessage();
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     This is Derived1!!
   └───────────────────────────────────┘
  
  ・純粋仮想関数を使用すれば、派生クラスで意味のある実装が行われなかったときに
   コンパイルエラーで、その不具合を検出することができます。
派生クラスに実装を
   任せている場合、このような視点でも抽象クラスを使用することには意味がありま
   す。
   

(*1)厳密に言えば可能ですが、キャストを悪用する必要があり、プログラムとしてのエレ
  ガントさに欠けることから普通は使用しません。
     ((Derived*)p_base)->DerivedExe();

(*2)「仮想関数にできる」と言うよりは「仮想関数にすべき」という表現が正しいでしょ
  う。以下の例では、基本クラスのデストラクタを仮想関数としなければ、派生クラス
  のデストラクタが動作しません。仮想関数を使用したクラスによる多態性利用時の
  頭ミスなので注意しましょう。

   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    class Base {
      public:
       Base() { cout << "Base Constructor\n"; }
       virtual ~Base() { cout << "Base Destructor\n"; } //仮想関数!!
       virtual void GetMessage() { cout << "This is Base!!\n"; }
    };
    
    class Derived : public Base {
      public:
       Derived() { cout << "Derived Constructor\n"; }
       ~Derived() { cout << "Derived Destructor\n"; }
       void GetMessage() { cout << "=== This is Derived!! ===\n"; }
    };
    
    Base* CreateObj(int i_flag);
    
    int main()
    {
      Base* p_base = CreateObj(1);
      p_base->GetMessage();
      
      delete p_base;
      return 0;
    }
    
    Base* CreateObj(int i_flag) {
      if (i_flag) return new Derived;
      return new Base;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [~Base()がvirtualの場合]    [~Base()がvirtualではない場合]
    ┌─────────────┐ ┌─────────────┐
    │Base Constructor     │ │Base Constructor     │
    │Derived Constructor    │ │Derived Constructor    │
    │=== This is Derived!! === │ │=== This is Derived!! === │
    │Derived Destructor    │ │Base Destructor      │
    │Base Destructor      │ └─────────────┘
    └─────────────┘
   └───────────────────────────────────┘

(*3)関数名はポインタであると考えれば、NULL領域を指している状態にしてあるだけです。
  ポインタ演算式中の0はNULLに読み替えられる。ただし、原著では「0」定義です。


[Revision Table]
 |Revision |Date    |Comments
 |----------|-----------|-----------------------------------------------------
 |1.00   |2002-07-22 |初版
 |1.01   |2002-07-26 |語句修正
 |1.02   |2002-07-28 |リンク追加
[end]
Copyright(C) 2002 Altmo