Cpp クラス 継承

提供:yonewiki
2022年9月26日 (月) 11:25時点におけるYo-net (トーク | 投稿記録)による版 (ページの作成:「C++に戻る <table class="mbox-small" style="border:1px solid #aaa; background-color:#f9f9f9; width:22em;" id="RealTitleBanner"> <tr> <td style="width:1px;"></td> <td class="mbox-text plainlist" style="">本来の表記は「<b><span id="RealTitle" style="font-size:large;">C++(Cpp) クラス 継承</span></b>」です。この記事に付けられた題名は{{記事名の制約}}から不正確なものとなっています。</…」)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)

C++に戻る


本来の表記は「C++(Cpp) クラス 継承」です。この記事に付けられた題名はテンプレート:記事名の制約から不正確なものとなっています。

※このページではC++にのみ存在する機能として、記事タイトルがC++ クラス 継承になっています。

クラス 継承

 根幹となる主要な処理や機能をもったクラスに対して、そのクラスをうまく使うようなクラスを作ったり、根幹となるクラスをもとに、いくつかの機能をもつクラスを作ったりするのに使う技術が継承です。英語ではインヘリタンス(inheritance)と言います。根幹となるクラスは基底クラスと呼ばれていて、その基底クラスを内包する新しく作るクラスを派生クラスと言います。


 初めて継承という言葉を聞く人はそれって便利なの?って思ったかもしれない。なぜなら、若かりし頃、自分もそれって便利なのみたいな感じでした。継承の技術だけを教えてもらったからなのかもしれない。具体的な活用例って大事なんだなと思うところです。だがしかし!具体的な利用例を考えると異常にながくなるのがサンプルプログラム。継承の説明をしているので、サンプルプログラム全体を見通す必要があり、バランスが大事。カンタンなサンプルで且つ、継承が有意義なプログラムに感じる説明が必要になるのです。でも有意義なプログラムって難しくなり勝ちだから、やっぱ、簡便なものを最初は紹介したいと思います。つまり、サンプルは技術を知るためだけの悪い使い方です。有意義な使い方についてはかなり先になってから、もう一度考えようかなって思ってます。


 以下のような毎月のサブスクリプション(月額契約)金額と使用した月数を受け取って、使用した月における合計金額を算出するクラスがあったとします。モノは言いようで、ただの掛け算をするだけのクラスです。


cpp (BaseInheritance.h)

#ifndef __BASEINHERITANCE_H_YONET__
#define __BASEINHERITANCE_H_YONET__
#if _MSC_VER > 1000
#pragma once
#endif

class CBaseInheritance {
protected:
    int m_iBaseMoney = 0;
    int m_iBaseMonth = 0;
    int m_fiBaseSumMoney();
public:
    CBaseInheritance();
    CBaseInheritance(int iArgBaseMoney, int iArgBaseMonth);
    ~CBaseInheritance();
    void m_fvBaseDispValue();
};
#endif


cpp (BaseInheritance.cpp)

#include <cstdio>
#include "BaseInheritance.h"

CBaseInheritance::CBaseInheritance() {
    printf("Constructor:CBaseInheritance()\n");
}

CBaseInheritance::CBaseInheritance(int iArgBaseMoney, int iArgBaseMonth) {
    m_iBaseMoney = iArgBaseMoney;
    m_iBaseMonth = iArgBaseMonth;
    printf("Constructor:CBaseInheritance(int,int)\n");
}
CBaseInheritance::~CBaseInheritance() {
    printf("Destructor:~CBaseInheritance()\n");
}
void CBaseInheritance::m_fvBaseDispValue() {
    int iSumMoney = m_fiBaseSumMoney();
    printf("BaseMoney=%d\n", iSumMoney);
}
int CBaseInheritance::m_fiBaseSumMoney() {
    return (m_iBaseMoney * m_iBaseMonth);
}

 上記のクラスを呼び出すだけで、掛け算をした結果が得られます。

cpp (BaseInheritanceMain.cpp)

#include <cstdio>

int main() {
    CBaseInheritance objCBaseInheritance(3900, 12);
    objCBaseInheritance.m_fvBaseDispValue();
    return 0;
}


処理結果

Constructor:CBaseInheritance
Money=46800
Destructor:~CBaseInheritance


 と上記のようなメインクラスでの利用をすれば、合計金額が表示されます。これに派生クラスを作りましょう。イメージとしては、この毎月の支払にオプションの利用料金が取られることを考えたようなクラスにしましょう。毎月毎月発生する金額を計算するということが根底にあって、その上にオプション料金を計算するという機能を付け足すので一応継承した感じにしました。ですが、簡単に言えば、合計金額にオプションの金額を足して表示するだけです。機能は何も利用しないので、おそろしくダメな継承ですね。基底クラスになった場合にコンストラクタの派生したクラスの引数側で呼び出すカタチが異なってもいいように、引数無しの何もしないコンストラクタを準備しておきました。


 勉強だし、簡単なので、やってみましょう。まずは派生させたヘッダファイルを作ります。  

cpp (DeriveInheritance.h)

#ifndef __DERIVEINHERITANCE_H_YONET__
#define __DERIVEINHERITANCE_H_YONET__
#if _MSC_VER > 1000
#pragma once
#endif
#include "BaseInheritance.h"

class CDeriveInheritance :public CBaseInheritance {
private:
    int m_iOptionMoney = 0;
    int m_iDeriveMoney = 0;
public:
    CDeriveInheritance(int iArgDeriveMoney, int iArgDeriveMonth, int iArgOptionMoney);
    ~CDeriveInheritance();
    void m_fvDeriveDispValue();
    int m_fiDeriveSumMoney();
};
#endif


 こんな感じの関数を定義してみようと思います。この中で新しい表記は一か所だけです。507行目の


class CDeriveInheritance:public CBaseInheritance{

 この部分が継承を定義している部分です。:の後ろに基底クラス名を記述しています。:の前は派生クラス名ということになります。基底クラス名の後ろにpublicというキーワードがありまして、基底クラスの中で設定されているアクセス指定子(private、protected、public)を一括で制限させる役割があります。publicに設定しておけば、基底クラスのアクセス指定子どおりになります。基底クラスと派生クラスは一つのクラスのような状態になりますが、別々のクラスとして区分けがあります。なので基底クラスでprivateとされていれば、派生クラスからはアクセスできません。protectedにはアクセスできます。public以外に設定するとアクセス指定子が派生クラスでの扱いが変わり、派生クラスの外への権限を一括変更できます。継承におけるアクセス指定子の働きの具体的な扱いの細かい部分ははこの先で紹介するクラスの機能を踏まえると複雑なので、ここでは深く触れないことにします。基底クラスのアクセス指定子を変更できるとだけと覚えておくのがよいと思います。ここまでの知識しかない現状では、そういうことにしておきましょう。


 メインプログラムのファイル名は何でも良いので、InheritanceMain.cppという名前のファイルにmain関数を置きました。main関数はなんでmainという名前なの?とかwmainって何とか、試しに一度Windowsアプリのプロジェクトを作ったら自動で作られたプログラムのmain関数にあたる部分がなぜかWinMainという名前になってmainという名前が使えなかったことがあるという人はVC PlusPlus:サブシステムとエントリポイントにmain関数の名前を変えることができるという内容の文献があります。main.cppという名前だったり、プロジェクト名と同じだったり、好きなファイル名にするといいと思います。Consoleアプリプロジェクトの規定値としてはVisualStudioC++のコンパイラはプロジェクトに置かれたプログラムファイル群からmainという名前の関数を探して、探し出したmain関数から始まるプログラムとして、解釈してくれます。リンク先の記事のように名前を変えることも出来るので、気に入らない人は、替えてみるのもよいですね。


=メンバ変数の利用用途=

  • m_iOptionMoney:オプションで追加購入したものの金額を覚えるものです。最初のコンストラクタで3番目の引数にあたるところの、1つ分のオプション価格であるint型の値しか受け取らないので、一つしかオプションは買えないっす。
  • m_iDeriveMoney: オプションも含めた合計金額を覚えます。基底の基本料金+利用月の合計に上乗せして計算する新しい機能の変数って感じはします。


=メンバ関数の利用用途=

  • m_fvDeriveDispValue():派生した方法での合計金額の中身を知るための関数
  • m_fiDeriveSumMoney(): 合計金額を取り出す関数

 という感じです。 そうすると、派生したクラスのプログラム部分のファイルの中身は以下のようになります。

cpp (DeriveInheritance.cpp)

#include <cstdio>
#include "DeriveInheritance.h"

CDeriveInheritance::CDeriveInheritance(int iArgDeriveMoney, int iArgDeriveMonth, int iArgOptionMoney) {
	printf("Constructer:CDeriveInheritance(int,int,int)\n");
	m_iBaseMoney = iArgDeriveMoney;
	m_iBaseMonth = iArgDeriveMonth;
	m_iOptionMoney = iArgOptionMoney;
}

CDeriveInheritance::~CDeriveInheritance() {
	printf("Destructer:~CDeriveInheritance()\n");
}

void CDeriveInheritance::m_fvDeriveDispValue() {
	int iSumMoney = m_fiDeriveSumMoney();
	printf("DeriveMoney=%d\n", iSumMoney);

}

int CDeriveInheritance::m_fiDeriveSumMoney() {
	return m_fiBaseSumMoney() + m_iOptionMoney;
}


 派生クラスからは基底クラスのアクセス指定子private以外の変数や関数が呼び出せます。基底クラスにどんな変数や関数があるのか知ってないと使えないので、基底クラスがどういう変数や関数を提供しているか、その働きはどうなっているかは、よく知っておく必要があります。基底クラスを知らずに、基底クラスの変数m_iBaseMoneyとか、m_iBaseMonthとかを操ることはできないです。ちなみに、今、あげた2つの変数はアクセス指定子がprotectedになっているので、派生したクラスなら呼び出していいことになっています。3つの引数をうけとる派生クラスのコンストラクタCDeriveInheritance::CDeriveInheritanceは2つを基底クラスのメンバ変数で覚えさせて、1つを派生クラスの変数に覚えさせています。


 CDeriveInheritance::m_fiDeriveSumMoney()関数は基底クラスのメンバ関数から合計金額の値を取得しつつ、派生クラスが覚えている金額と足し合わせている感じです。


 あとは、派生クラスのデストラクタCDeriveInheritance::~CDeriveInheritanceだったり、変数の中身をお披露目するCDeriveInheritance::m_fvDeriveDispValue()は、特に凝ったことはしていないので、説明しません。



 このような派生クラスをメインプログラムから、以下のように利用します。


cpp (InheritanceMain.cpp)

#include <cstdio>
#include "DeriveInheritance.h"

int main() {

    CDeriveInheritance objCDeriveInheritance(3900, 12, 600);
    objCDeriveInheritance.m_fvDeriveDispValue();

    return 0;
}


 とすると、実行結果は

処理結果

Constructor:CBaseInheritance()
Constructer:CDeriveInheritance(int,int,int)
DeriveMoney = 47400
Destructer:~CDeriveInheritance()
Destructor:~CBaseInheritance()


 のようになります。派生クラスを作成すると、基底のコンストラクタ引数無しの後に派生のコンストラクタ引数(int, int, int)が呼ばれて、メイン関数から、派生クラスの中身を表示させる命令を実行したあと、メイン関数がおわろうとする手前で、派生クラスのデストラクタが呼ばれて、基底クラスのデストラクタが呼ばれて終わっていることがわかります。


 派生クラスを生成させたときに派生クラス側で引数がある場合、派生クラスはもちろん自身のコンストラクタが呼ばれますが、基底クラスでは、何もないコンストラクタが呼ばれます。基底クラスで、引数付きのコンストラクタが存在するクラスを作っていた場合で派生クラスで初期化がされない事がある場合、引数のないコンストラクタを明記しないとダメです。最初から基底クラスにコンストラクタが存在しないなら、引数のないコンストラクタを明記しておく必要はないです。基底クラスが引数付きコンストラクタで初期化処理するクラスで、派生クラスから呼ばれても影響が無いことを証明するようなものです。あるいは影響があっても派生クラスがちゃんとやってくれるんなら許すという感じでもあります。基底クラスになるにしても、基底クラスになる準備が出来ているか?というものは、求められていることになります。今回の場合は、呼び出されたあとに、値が設定されたかが重要になるので、基底のクラスのコンストラクタで、値を受け取って、基底のクラスが使える状態になったか?というのは基底のコンストラクタで記述しないとダメなのかもしれませんが、勉強用なので、特に何もしませんでした。ほったらかしです。


 今回のサンプルのように、継承という手法は基底側と派生側とが完璧な関係でないパターンは良くない継承の使い方にも見えます。継承の機能は使い方が大事ということは、なんとなくわかったかなぁと思います。もっといい継承の使い方は、全てを理解したあとで、考えましょう。プログラムに正解はないので、よりよいと思われるとか、あまりよくないとか、本来の使い方とは違うとかいろいろあるのかもしれませんが、目的は達成してしまうことがあるのがプログラムです。後から、機能を追加したり、見直したりするときにわかりやすいとか、いろいろな要素があるので、完璧というのは、難しいです。言語仕様がわかっていて、結果もでていて、まぁメンテナンスもできるし、いいんじゃないかなぁというプログラムでも誰かが見れば、いや、これはこういう使い方しない方がよいのでは?と思われるくらい、奥がふかいので、ここでは誰もが納得のいく答えが提供できる気がしないですが、もう少しくらいは、ゆっくり考えてみたいと思います。後で。(先延ばし主義)


 メイン関数からは、派生クラスが基底クラスを使っているかどうかは、わからないくらいの状態でプログラムされています。インクルードしているヘッダファイルも基底クラスを直接使わない分には、基底クラス用のヘッダファイルを読み込む必要もないです。基底クラスがすべてのことの根幹をなしていて、重要な機能を成しているほど、それをもとに作った、派生したクラスは基底クラスがあって有り難いはず。そして派生クラスは、基底クラスに機能を追加して、充実したものになる。そういう関係が大事だと思います。


C++に戻る