目次

■ 継承

go back to frame topframe top

クラスの継承について解説します。

0. 目次

  1. 継承の概論
  2. 継承を記述する
  3. 継承クラスのオブジェクトを使う
  4. 補足事項
  5. 演習問題

1. 継承の概論

C++ におけるクラスとオブジェクトは、定義と存在のアナロジーであることを『クラスとオブジェクト』にて述べました。定義が『鳥』であるならば、「雀」「燕」「鶏」がその存在でしたね。この関係がクラスとオブジェクトと言うわけでした。ここで述べる継承の話題は主に定義の仕方に関係してきます。

1.1. 定義について考える

私達は定義を作る場合、例えば図鑑を作るとき、しばしば階層構造を作ります。例えば動物図鑑を開くと以下のような図を作ることが出来るでしょう。

この例はだと分類学的で細かすぎる感じですが、少なくとも秋田犬も柴犬も全部「イヌ」に分類しますよね。こういった定義の分類は、現実事象の記述方法として常套的です。この方法論をC++クラスの設計に取り込んだのがここで述べる「継承」となるわけです。それでは、『クラスとオブジェクト』で実装した本屋さん管理システムで考えて見ましょう。

1.3. 本屋さんシステムにおける継承

本屋さんにある本全てをデータ化するべく、システムにオブジェクトを作成していきましょう。CBook にはタイトル・筆者・価格・入荷数・売り上げ数があります。ところが、本屋さんには雑誌と言うものもあって、これには筆者と言うものはありません。代わりに「○年△月号」と言う発売号数と言う付随データがあります。今回作成した CBook ではこれを表現できません。実は本屋さんにある本データは以下のような継承関係があったのです。

この継承関係を次章からC++プログラムに落としてみましょう。

2. 継承を記述する

1.3 で検討した継承図を C++ 的に実装してみましょう。

2.1. 親クラスの検討

前回の CBook から少なくとも雑誌には必要ない著者属性(m_pszAuther)とその操作を削除して、これを 1.3. の継承図における『本』としてみましょう。以下のような形になると思います。

// 書籍クラス
class CBook
{
protected:

    const char* m_pszTitle;         // タイトル
    const int   m_nPrice;           // 値段(円)
    const int   m_nArrivedValue;    // 入荷した数
    int         m_nSelledValue;     // 売れた数

    // -----------------------------------
    // Summary:
    //  売り上げの算出
    // Returns:
    //  売り上げ額(円)
    int GetSells() const
    {
        return m_nSelledValue * m_nPrice;
    }

    // -----------------------------------
    // Summary:
    //  残数の算出
    // Returns:
    //  残数
    int GetRestValue() const
    {
        return m_nArrivedValue - m_nSelledValue;
    }

public:

    // -----------------------------------
    // Summary:
    //  コンストラクタ
    // Arguments:
    //  pszTitle:       タイトル
    //  pszAuther:      著者
    //  nPrice:         値段
    //  nArrivedValue:  入荷した数
    //  nSelledValue:   売れた数
    CBook( const char* pszTitle,
        const int nPrice, int nArrivedValue, int nSelledValue )

    :m_pszTitle( pszTitle ),
        m_nPrice( nPrice ), m_nArrivedValue( nArrivedValue )
    {
        m_nSelledValue = nSelledValue;
    }

    // -----------------------------------
    // Summary:
    //  デストラクタ
    virtual ~CBook(){}

    // -----------------------------------
    // Summary:
    //  一冊売れた
    void Selled()
    {
        m_nSelledValue++;
    }

    // -----------------------------------
    // Summary:
    //  状態の表示
    void Disp()
    {
        
        cout << m_pszTitle << "のデータ" << endl;
        cout << "--------------------------" << endl;
        cout << "値段:" << m_nPrice << endl;
        cout << "入荷した冊数:" << m_nArrivedValue << endl;
        cout << "販売した冊数:" << m_nSelledValue << endl;
        cout << "売り上げ:" << GetSells() << endl;
        cout << "残冊数:" << GetRestValue() << endl;
    }
};

2.2. 子クラス<一般書籍>を設計する

1.3. の継承図の『一般書籍』には、新しい CBook に比べて著者名のサポートが必要です。元の CBook と同等にするわけです。一般書籍クラスを CGeneralBook として、CBook から継承を行ってみましょう。

// 一般書籍クラス
class CGeneralBook : public CBook
{
protected:
    const char* m_pszAuther;        // 筆者

public:
    // -----------------------------------
    // Summary:
    //  コンストラクタ
    // Arguments:
    //  pszTitle:       タイトル
    //  pszAuther:      著者
    //  nPrice:         値段
    //  nArrivedValue:  入荷した数
    //  nSelledValue:   売れた数
    CGeneralBook( const char* pszTitle, const char* pszAuther,
        const int nPrice, int nArrivedValue, int nSelledValue )

        :CBook( pszTitle, nPrice, nArrivedValue, nSelledValue ),
            m_pszAuther( pszAuther )
    {
    }

    // -----------------------------------
    // Summary:
    //  デストラクタ
    virtual ~CGeneralBook(){}

    // -----------------------------------
    // Summary:
    //  状態の表示
    void Disp()
    {
        CBook::Disp();

        cout << "著者:" << m_pszAuther << endl;
        cout << endl;
    }

};

継承を用いると、全てのメンバ変数・メンバ関数は子クラスに引き継がれます。記載するのは、増えたメンバ変数・メンバ関数、変更したいメンバ変数、コンストラクタ・デストラクタです。具体的にはこの色で示唆された箇所となります。それぞれ見て行きましょう。

2.2.1. 子クラスと継承関係を定義する

子クラスとなるクラス名とその親クラスのクラス名を以下の箇所で定義しています。

// 一般書籍クラス
class CGeneralBook : public CBook
{
	…
};

意味は『CBook から public で継承された、CGeneralBook』と言う意味です。継承には属性があって、public な継承、protected な継承、private な継承があり、この例では public です。それぞれ子クラスにおける親クラスから引き継いだメンバの扱いを定義するものですが、まずは普通に引き継ぐ(親クラスの扱いがそのまま引き継がれる、)public だけ抑えてください。

2.2.2. 子クラスで増やしたいメンバ変数を定義する

CGeneralBook は CBook に比べて「著者」メンバ変数が増えています。親クラスで既に定義されているメンバ変数を記述する必要はありません。以下の箇所で「著者」メンバ変数が追加されています。

// 一般書籍クラス
class CGeneralBook : public CBook
{
protected:
    const char* m_pszAuther;        // 筆者
	
	…
};

同様に子クラスでメンバ関数を追加することもできます。このことはインターフェースの拡張が可能であることを意味します。

2.2.3. コンストラクタの継承関係を定義する

『クラスとオブジェクト』『2.2.2.3. コンストラクタ・デストラクタを実装』で述べたコンストラクタ呼び出しの継承を定義しなければなりません。要は子クラスのコンストラクタから親クラスのコンストラクタを呼び出す、と言うことです。親クラスのコンストラクタを継承しないと、親クラスが前提としている初期化処理が行われません。これに対しての保障を C++ では言語レベルでサポートしている、と。そういうわけです。

具体的には以下の箇所でコンストラクタの定義が行われています。

// 一般書籍クラス
class CGeneralBook : public CBook
{
	…

public:
    // -----------------------------------
    // Summary:
    //  コンストラクタ
    // Arguments:
    //  pszTitle:       タイトル
    //  pszAuther:      著者
    //  nPrice:         値段
    //  nArrivedValue:  入荷した数
    //  nSelledValue:   売れた数
    CGeneralBook( const char* pszTitle, const char* pszAuther,
        const int nPrice, int nArrivedValue, int nSelledValue )

        :CBook( pszTitle, nPrice, nArrivedValue, nSelledValue ),
            m_pszAuther( pszAuther )
    {
    }

	…
};

「,」 で区切って『クラスとオブジェクト 2.2.2.3. コンストラクタ・デストラクタを実装』同様にメンバ変数の初期化を記述することができます。

2.2.4. 動作を変更するメンバ関数を定義する

親クラスのうち、動作を変更したいメンバ関数を定義します。その場合、親クラスと同一名、同一型、同一引数のメンバ関数を定義することで可能となります。

// 一般書籍クラス
class CGeneralBook : public CBook
{
	…
    // -----------------------------------
    // Summary:
    //  状態の表示
    void Disp()
    {
        CBook::Disp();

        cout << "著者:" << m_pszAuther << endl;
        cout << endl;
    }
};

この例の場合だと、CBook::Disp() メンバ関数を変更して、著者の表示をサポートしています。このような動作を『オーバーライド』と言います。オーバーライド前のメンバ関数を呼びだい場合、親クラスの名前を頭につけて、CBook::Disp(); のように呼び出します。 この例では、CBook の表示ルーチンを呼び出した上で、拡張された著者情報表示を CGeneralBook::Disp() で行っています。

2.3. 子クラス<雑誌>を設計する

1.3. 本屋さんシステムにおける継承 の継承図に出てくる「雑誌」は、更に子であるところの「季刊誌」「月刊誌」「週刊誌」を持っています。話の流れでは「雑誌」クラスを設計して、その子クラスを更に設計するわけですが、今回はプログラマが「雑誌以下のクラスには継承するほどの改変量がない」と判断したとしましょう。そういう場合、「属性で回避する」方法があります。属性とは早い話がメンバ変数のことです。ちなみにメンバ関数を「操作」と言ったりすることもあります。

2.3.1. 属性で回避する

子クラスを作らずに、雑誌クラスだけで、季刊誌、月刊誌、週刊誌をサポートしたい場合、例えばメンバ変数に const な int 型の変数を持って 0 = 季刊誌、1 = 月刊誌、2 = 週刊誌と言う初期化をさせ、操作の際、各々動作を分岐する、と言う方法があります。そのような CBook の子クラス CMagazine を以下に設計していきます。まずは、継承ですから、以下のような形となります。

// 一般書籍クラス
class CMagazine : public CBook
{
	…
};

2.3.2. 季刊誌、月刊誌、週刊誌 属性を持たせる

ここで、もちろん const int なメンバ変数を定義しても良いのですが、このクラスを使う(別の)プログラマが 0、1、2 以外の数値で初期化する可能性があるのは、多少気分がよろしくありません。そのような場合、C 言語的なアプローチですが enum で数値型を定義してしまう方法があります。なお、今回雑誌には第何号(何月号、何週号)もメンバ変数で初期化することにしましょう。

// 雑誌クラス
class CMagazine : public CBook
{
public:

    enum Period{
        Weekly = 52,    // 週刊誌
        Monthly = 12,   // 月刊誌
        Quarterly = 4   // 季刊誌
    };

protected:

    const Period    m_Period;   // 雑誌発売時期(週間/月間/季刊)
    const int       m_nNumber;  // 雑誌ナンバー

public:


    // -----------------------------------
    // Summary:
    //  コンストラクタ
    // Arguments:
    //  pszTitle:       タイトル
    //  nPrice:         値段
    //  nArrivedValue:  入荷した数
    //  nSelledValue:   売れた数
    //  period:         雑誌発売時期
    //  nNumber:        雑誌ナンバー
    CMagazine( const char* pszTitle, const int nPrice,
        int nArrivedValue, int nSelledValue, Period period, int nNumber )

    :CBook( pszTitle, nPrice, nArrivedValue, nSelledValue ),
     m_Period( period ), m_nNumber( nNumber )
    {

        ASSERT( ( nNumber >= 1 ) && ( nNumber <= period ) );
    }

    virtual ~CMagazine(){}

};
2.3.3.1. enum を使う

enum は列挙型と呼ばれる型定義で、Period 型のインスタンス m_Period はこの例だと Weekly、Monthly、Quarterly のいずれか以外の指定を行うと、コンパイルエラーになります。実態は数値で、それぞれ 52、12、4 となっています。なお、これら数値は雑誌ナンバー m_nNumber の最大値と同じにしてしまいました(あまり例として適当ではないかも知れません)。理由は後で分かります。この型宣言はクラスの入れ子にすることができますが、今回外側からコンストラクタで初期化したいので、public にしています。

2.3.3.2. コンストラクタで属性を初期化する

コンストラクタで m_Period と m_nNumber (発売時期と雑誌ナンバー)を初期化しています。ここで、m_Period は enum 型を用いることで不正な値が入ることを防いだわけですが、m_nNumber は m_Period に依存して最大値がことなります。季刊は 1 〜 4 ですし、月刊なら 1 〜 12。週刊なら最大 52 号です(今回の例では年単位でデータベースを持つものと仮定しています)。C++ の講座ですが、ここは MFC のマクロを使ってこのチェックを行わせて見ましょう。それが以下のコードです。

        ASSERT( ( nNumber >= 1 ) && ( nNumber <= period ) );

ASSERT マクロの使用方法については、実装パターンの『MFC デバッグマクロの使用』を参照ください。ASSERT の中には有効条件を書きます。条件に不適合となると、デバッグモードでのみ、その通知(場所と内容)を行うダイアログボックスが表示され、ロジックのエラーを早期に発見できます。なお、標準 C++ にも assert と言う名前で同様の機能があります。

なお、このように属性を enum で指定する場合、その引数は以下のように記述します。

        CMagazine       WeeklyASCII( "週刊アスキー",
                            350, 20, 0, CMagazine::Weekly, 28 );

この enum を使った属性記述方法は MFC でも使用されていますので、やり方ぐらいは記憶に留めておくと良いでしょう。

2.3.3.3. 余談

Period 型を各雑誌属性の最大号数とイコールにした理由です。本質的には以下のように m_Period の状態に依存して書くのが良いのですが、

// デバッグモードのみコンパイル
#ifdef	_DEBUG

        switch( m_Period ){
        case Weekly:
            ASSERT( ( nNumber >= 1 ) && ( nNumber <= 52 ) );
            break;

        case Monthly:
            ASSERT( ( nNumber >= 1 ) && ( nNumber <= 12 ) );
            break;

        case Quarterly:
            ASSERT( ( nNumber >= 1 ) && ( nNumber <= 4 ) );
            break;

        default:
            ASSERT(0);  // 必ず Assert。
            break;
        }

#endif

VC++ では デバッグモードでコンパイルするとき、_DEBUG が必ず define されているので、それを用いています。しかし、これはいくらなんでもマクロに頼りすぎていて冗長かな、と。あるいは各m_Period状態に応じて最大ナンバー値を属性で持っても良いかも知れません。

2.3.4. 表示インターフェースの実装

CGeneralBook で行ったように、表示を行うインターフェース CBook::Disp() はオーバーライドが必要です。今回、属性による解決を行っていますので、以下のように分岐コードが必要です。

// 雑誌クラス
class CMagazine : public CBook
{
public:

    enum Period{
        Weekly = 52,    // 週刊誌
        Monthly = 12,   // 月刊誌
        Quarterly = 4   // 季刊誌
    };

protected:

    const Period    m_Period;   // 雑誌発売時期(週間/月間/季刊)
    const int       m_nNumber;  // 雑誌ナンバー

public:


    // -----------------------------------
    // Summary:
    //  コンストラクタ
    // Arguments:
    //  pszTitle:       タイトル
    //  nPrice:         値段
    //  nArrivedValue:  入荷した数
    //  nSelledValue:   売れた数
    //  period:         雑誌発売時期
    //  nNumber:        雑誌ナンバー
    CMagazine( const char* pszTitle, const int nPrice,
        int nArrivedValue, int nSelledValue, Period period, int nNumber )

    :CBook( pszTitle, nPrice, nArrivedValue, nSelledValue ),
     m_Period( period ), m_nNumber( nNumber )
    {

        ASSERT( ( nNumber >= 1 ) && ( nNumber <= period ) );
    }

    virtual ~CMagazine(){}

    // -----------------------------------
    // Summary:
    //  状態の表示
    void Disp()
    {

        CBook::Disp();

        switch( m_Period ){
        case Weekly:

            cout << "発売時期:週刊" << endl;
            break;

        case Monthly:

            cout << "発売時期:月刊" << endl;
            break;

        case Quarterly:

            cout << "発売時期:季刊" << endl;
            break;

        default:

            ASSERT(0);
            break;
        }
        cout << endl;
    }
};

これも表示する "発売時期:XX"の XX の部分の文字列を コンストラクタで m_Period に依存して初期化される文字列メンバ変数として設計しても良いでしょう。

2.3.5. CMagazine の設計を通して感じて欲しいこと

ここまで属性による解決法を用いた実装参考例を書いてきましたが、実は継承でない属性と分岐による解決は、この規模でさえメンバ関数内のロジックがややこしくなったり、管理するメンバ変数を増やしてしまう可能性があります。ソースコードの可読性を優先できるのであれば、積極的に継承は使ってみるべきです。仮に月刊誌から下の継承関係に女性誌、男性誌が増え、週刊誌の下には芸能誌、経済誌が増えたとしたら、現在設計した CMagazine は破綻してしまいます。

クラス設計時のきめ細かな継承関係の検討は、結果的にプログラマの負担を減らすのです。

3. 継承クラスのオブジェクトを使う

継承されたクラスのオブジェクトであってもオブジェクトの使用方法に変わりはありません。以下に使用例を示します。

int main()
{
    CGeneralBook    HarryPotter( "ハリーポッター賢者の石",
                        "J.K.ローリング",1900, 52, 0 );

    CMagazine       Gendai( "月間現代",
                        780, 8, 0, CMagazine::Monthly, 10 );

    CMagazine       WeeklyASCII( "週刊アスキー",
                            350, 20, 0, CMagazine::Weekly, 28 );

    CMagazine       Number( "Number",
                        950, 10, 0, CMagazine::Weekly, 3 );

    // ハリーポッター1冊売れた
    HarryPotter.Selled();
    HarryPotter.Disp(); // 売り上げ表示

    // 月刊現代1冊売れた
    Gendai.Selled();
    Gendai.Disp();

    // 週刊 ASCII 1冊売れた
    WeeklyASCII.Selled();
    WeeklyASCII.Disp();

    // 週刊 Number 1冊売れた
    Number.Selled();
    Number.Disp();

    return -1;
}

実行結果は以下の通りです。確かに継承され、オーバーライドされた Disp() メンバ関数が動作していることに注目して確認してみてください。

4. 補足事項

以下、補足事項です。

4.1. 「鶏」は本当にオブジェクトか?

『クラスとオブジェクト』で、「鳥」をクラス、「雀」「燕」「「鶏」をオブジェクト、と言う参考例を出してみました。しかし、これは本当でしょうか。「雀」と「燕」は飛べますが、「鶏」は飛べません。今回述べた継承関係図を作ることができるわけで、実は「鶏」まで現実事象では「定義」です。本当のオブジェクトとは、鶏に「コケ」と「コッコー」と名前をつけて存在している方がオブジェクトじゃありませんか。

が、そういうことを言い出すとですね。じゃあ、クローン鶏が作れるとして、「コケ」から「コケ・クローン1」「コケ・クローン2」が生まれるようなケースが現れたら、名前を付けた「コケ」までが定義で、存在をクローンに割り当てた方が良い、などと。

クラスとするか、オブジェクトとするか、と言う判断は、作成するシステムに対して依存します。今回の例では、雑誌クラスの設計がそのことを考える好例となるでしょう。この点に迷った場合、勘所としては抽象化されたものの違いが、メンバ変数の中身による違いだけなのか、メンバ変数の種類、メンバ関数実装の違いにまで及ぶのか、と言うことを検討してみるとそれがクラスなのか、オブジェクトなのかが見えてくると思います。

5. 演習問題

八百屋さん計算システムを以下の仕様とガイドラインにしたがってコマンドライン・アプリケーションとして設計してください。

仕様:

  1. りんご、バナナ、トマト(野菜)、にんじんが商品である。
  2. りんごの仕入れ値は300円、定価は400円。
  3. バナナの仕入れ値は200円、定価は300円。
  4. トマトの仕入れ値は30円、定価は50円。
  5. にんじんの仕入れ値は50円、定価は70円。
  6. それぞれ仕入れ数を入力すると、総仕入額が表示
  7. 売り上げがある度にお客様一人のお買い上げ金額と総売り上げ、各品物の残数を表示。
  8. いつも全ての品物が5%引きである。
  9. 火曜日は全ての果物は値引き額から更に10%引きである。
  10. りんごは5個パックで買うと 値引き額からさらに5%引きである。
  11. バナナは3房パックで買うと、値引き額からさらに3%引きである。
  12. 木曜日全ての野菜は4個買うともう1個おまけで付いてくる。

ガイドライン:

  1. 各品物はクラスとして設計してください。
  2. 事前に必要なクラスを列挙してみる。
  3. 継承図を使って継承関係がないか、検討してみる。
  4. 入力数値の範囲チェックは行わなくて良い。
  5. 仕入れ数入力と販売時の入力は別の状態として設計する。
  6. 品物の種類が増えることはないことを前提にして良い。
go back to frame topframe top