C++入門|継承とポリモーフィズム

基本戦士の力を受け継ぎ、治癒戦士は独自の技を放つ。C++の継承は、共通能力を親クラスにまとめ、子クラスで個性を追加するための強力な仕組みです。

C++のオブジェクト指向では、クラスを使ってインスタンスを作り、メンバ変数やメンバ関数をまとめて扱ってきました。

これまでの学習では、1つのクラスの中にデータと処理をまとめることを中心に見てきました。
しかし、プログラムが大きくなると、似たようなクラスをいくつも作りたくなる場面があります。

たとえば、ドラゴンボール風の修行世界で考えてみましょう。

基本となる戦士には、気の出力を設定したり、修行したり、修行ポイントを確認したりする共通機能があります。
でも、戦士の種類によっては、さらに特別な能力を持っていることがあります。

たとえば、治癒戦士は、基本的な修行能力に加えて、仲間を回復する技を持っています。

このように、共通する性質を親クラスにまとめ、そこから性質を受け継いだ子クラスを作る仕組みを、継承といいます。

考え方C++での名前ドラゴンボール風のイメージ
基本となるクラス親クラス、基底クラス、スーパークラスすべての戦士に共通する基本戦士
受け継いで作るクラス子クラス、派生クラス、サブクラス基本能力に独自技を加えた治癒戦士
性質を引き継ぐ仕組み継承基本修行能力を受け継ぐ
同じ指示で異なる動きをする考え方ポリモーフィズム戦士の種類ごとに技の出方が変わる

今回の記事では、まず継承の基本をしっかり確認します。

具体的には、BaseWarriorクラスを親クラスとして作り、そこからHealingWarriorクラスを子クラスとして作ります。

BaseWarriorは、気の出力や修行ポイントを管理する基本戦士です。
HealingWarriorは、BaseWarriorの機能を受け継ぎつつ、仲間を回復するhealAlly関数を追加した治癒戦士です。

ポリモーフィズムについては、この継承の考え方が土台になります。
また、今回のサンプルでは、将来ポリモーフィズムを安全に扱うために大切なvirtualデストラクタにも触れます。

図:親クラスの基本能力を子クラスが受け継ぐ

この図が示していること

この図では、BaseWarriorクラスの機能をHealingWarriorクラスが受け継ぐ様子を表しています。

BaseWarriorには、気の出力を設定するsetPower、修行するtrain、修行ポイントを取得するgetTrainingPointなどの共通機能があります。

HealingWarriorは、それらを受け継いだうえで、healAllyという独自機能を追加します。
これが継承の基本的なイメージです。

継承の概念

継承とは、あるクラスの性質を受け継いで、新しいクラスを作る仕組みです。

親となるクラスには、共通する機能をまとめます。
子となるクラスでは、その共通機能を受け継ぎながら、必要に応じて独自の機能を追加します。

ドラゴンボール風に考えると、BaseWarriorはすべての戦士が持つ基本修行能力をまとめたクラスです。

そこからHealingWarriorを作ると、HealingWarriorはBaseWarriorの能力を受け継ぎます。
そのため、HealingWarriorでもsetPowerやtrainを使えます。

さらに、HealingWarriorだけの能力としてhealAllyを追加できます。

クラス役割持っている機能
BaseWarrior親クラス気の出力設定、修行、修行ポイント取得
HealingWarrior子クラス親クラスの機能 + 仲間の回復

このようにすると、共通する処理を何度も書かなくて済みます。

BaseWarriorに基本機能をまとめておけば、HealingWarriorだけでなく、PowerWarriorやSpeedWarriorのような別の子クラスを作るときにも再利用できます。

親クラスと子クラス

継承では、継承元のクラスを親クラスと呼びます。

C++では、親クラスのことを次のように呼ぶこともあります。

呼び方意味
親クラス継承元のクラス
基底クラスベースになるクラス
スーパークラス上位のクラス

一方、親クラスを継承して作ったクラスは子クラスです。

子クラスにも、いくつかの呼び方があります。

呼び方意味
子クラス親から受け継いだクラス
派生クラス親から派生したクラス
サブクラス下位のクラス

今回のサンプルでは、次の関係になります。

役割クラス名イメージ
親クラスBaseWarrior基本戦士
子クラスHealingWarrior治癒戦士

HealingWarriorはBaseWarriorを継承するため、BaseWarriorでpublicに定義されたメンバ関数を利用できます。

継承の書き方

C++で継承を使うときは、子クラスの宣言で次のように書きます。

class 子クラス名 : public 親クラス名

今回のサンプルでは、次のようになります。

class HealingWarrior : public BaseWarrior

これは、HealingWarriorクラスがBaseWarriorクラスをpublic継承するという意味です。

public継承では、親クラスのpublicメンバは、子クラスでもpublicな機能として利用できます。

つまり、BaseWarriorでpublicにしているsetPowerやtrainは、HealingWarriorでも使えます。

今回作成するサンプルプログラム

今回のプログラムは、次の5つのファイルで構成します。

ファイル役割
base_warrior.hBaseWarriorクラスの宣言
base_warrior.cppBaseWarriorクラスの定義
healing_warrior.hHealingWarriorクラスの宣言
healing_warrior.cppHealingWarriorクラスの定義
main.cpp2つのクラスを生成して動作を確認する

BaseWarriorでは、基本的な修行処理を作ります。
HealingWarriorでは、BaseWarriorを継承し、回復技を追加します。

BaseWarriorクラスの宣言

まず、親クラスとなるBaseWarriorを宣言します。

プロジェクト/ファイル名: Chap5_01/base_warrior.h

#ifndef _BASE_WARRIOR_H_
#define _BASE_WARRIOR_H_

class BaseWarrior {
public:
    // コンストラクタ
    BaseWarrior();

    // デストラクタ
    virtual ~BaseWarrior();

    // 気の出力を設定する
    void setPower(double power);

    // 気の出力を取得する
    double getPower();

    // 修行ポイントを取得する
    double getTrainingPoint();

    // 指定した時間だけ修行する
    void train(double hour);

private:
    // 現在の気の出力
    double m_power;

    // 修行で蓄積したポイント
    double m_trainingPoint;
};

#endif // _BASE_WARRIOR_H_

このBaseWarriorクラスには、気の出力と修行ポイントを管理する機能があります。

メンバ種類内容
m_powerprivateメンバ変数現在の気の出力
m_trainingPointprivateメンバ変数修行で蓄積したポイント
setPowerpublicメンバ関数気の出力を設定する
getPowerpublicメンバ関数気の出力を取得する
getTrainingPointpublicメンバ関数修行ポイントを取得する
trainpublicメンバ関数指定時間だけ修行する

m_powerとm_trainingPointはprivateです。

そのため、クラスの外から直接アクセスできません。
子クラスであるHealingWarriorからも、直接m_powerやm_trainingPointを操作することはできません。

ただし、publicなメンバ関数であるsetPower、getPower、getTrainingPoint、trainは、子クラスでも利用できます。

BaseWarriorクラスの定義

次に、BaseWarriorクラスのメンバ関数を定義します。

プロジェクト/ファイル名: Chap5_01/base_warrior.cpp

#include "base_warrior.h"
#include <iostream>

using namespace std;

// コンストラクタ
BaseWarrior::BaseWarrior() : m_power(0.0), m_trainingPoint(0.0) {
    cout << "== BaseWarriorクラスのインスタンス生成 ==" << endl;
}

// デストラクタ
BaseWarrior::~BaseWarrior() {
    cout << "== BaseWarriorクラスのインスタンス消去 ==" << endl;
}

// 気の出力を設定する
void BaseWarrior::setPower(double power) {
    m_power = power;
}

// 気の出力を取得する
double BaseWarrior::getPower() {
    return m_power;
}

// 修行ポイントを取得する
double BaseWarrior::getTrainingPoint() {
    return m_trainingPoint;
}

// 指定した時間だけ修行する
void BaseWarrior::train(double hour) {
    cout << "気の出力" << m_power << "で" << hour << "時間修行" << endl;
    cout << m_power * hour << "ポイント修行しました。" << endl;

    // 修行ポイントを加算する
    m_trainingPoint += m_power * hour;
}

このクラスでは、コンストラクタでm_powerとm_trainingPointを0.0に初期化しています。

BaseWarrior::BaseWarrior() : m_power(0.0), m_trainingPoint(0.0) {
    cout << "== BaseWarriorクラスのインスタンス生成 ==" << endl;
}

train関数では、気の出力と修行時間から修行ポイントを計算しています。

m_trainingPoint += m_power * hour;

ドラゴンボール風に言えば、気の出力が高く、修行時間が長いほど、多くの修行ポイントが蓄積されるイメージです。

HealingWarriorクラスの宣言

次に、子クラスとなるHealingWarriorを作ります。

HealingWarriorはBaseWarriorを継承し、さらに仲間を回復するhealAlly関数を持ちます。

プロジェクト/ファイル名: Chap5_01/healing_warrior.h

#ifndef _HEALING_WARRIOR_H_
#define _HEALING_WARRIOR_H_

#include "base_warrior.h"

class HealingWarrior : public BaseWarrior {
public:
    // コンストラクタ
    HealingWarrior();

    // デストラクタ
    virtual ~HealingWarrior();

    // 仲間を回復する
    void healAlly();

private:
    // 治癒部隊の呼び出しコード
    int m_healCode;
};

#endif // _HEALING_WARRIOR_H_

この宣言で大切なのは、次の部分です。

class HealingWarrior : public BaseWarrior

これは、HealingWarriorがBaseWarriorを継承していることを表します。

そのため、HealingWarriorはBaseWarriorのpublicメンバ関数を使えます。

つまり、HealingWarrior自身にはsetPowerやtrainを直接定義していなくても、次のように呼び出せます。

pHealer->setPower(600);
pHealer->train(2.0);

さらにHealingWarriorには、独自の関数としてhealAllyを追加しています。

void healAlly();

これはBaseWarriorにはない、HealingWarriorだけの機能です。

HealingWarriorクラスの定義

次に、HealingWarriorクラスのメンバ関数を定義します。

プロジェクト/ファイル名: Chap5_01/healing_warrior.cpp

#include "healing_warrior.h"
#include <iostream>

using namespace std;

// コンストラクタ
HealingWarrior::HealingWarrior() : m_healCode(777) {
    cout << "** HealingWarriorクラスのインスタンス生成 **" << endl;
}

// デストラクタ
HealingWarrior::~HealingWarrior() {
    cout << "** HealingWarriorクラスのインスタンス消去 **" << endl;
}

// 仲間を回復する
void HealingWarrior::healAlly() {
    cout << "仲間を回復する治癒の気を放つ" << endl;
    cout << "治癒部隊の呼び出しコードは" << m_healCode << "番" << endl;
}

HealingWarriorでは、m_healCodeというprivateメンバ変数を持っています。

これは、治癒部隊の呼び出しコードを表す値です。

コンストラクタで777を設定しています。

HealingWarrior::HealingWarrior() : m_healCode(777) {
    cout << "** HealingWarriorクラスのインスタンス生成 **" << endl;
}

healAlly関数では、仲間を回復するメッセージと、呼び出しコードを表示します。

void HealingWarrior::healAlly() {
    cout << "仲間を回復する治癒の気を放つ" << endl;
    cout << "治癒部隊の呼び出しコードは" << m_healCode << "番" << endl;
}

main.cppで親クラスと子クラスを使う

最後に、main.cppを作成します。

ここでは、BaseWarriorとHealingWarriorをそれぞれ生成して、使えるメンバ関数の違いを確認します。

プロジェクト/ファイル名: Chap5_01/main.cpp

#include <iostream>
#include "base_warrior.h"
#include "healing_warrior.h"

using namespace std;

int main(int argc, char** argv) {
    cout << "-- BaseWarriorクラスの処理 --" << endl;

    // 基本戦士クラスのインスタンスを生成する
    BaseWarrior* pBase = new BaseWarrior();

    // 基本戦士の気の出力を設定する
    pBase->setPower(400);

    // 基本戦士が修行する
    pBase->train(1.5);

    // 基本戦士の総修行ポイントを表示する
    cout << "総修行ポイント:" << pBase->getTrainingPoint() << "ポイント" << endl;

    // 基本戦士クラスのインスタンスを消去する
    delete pBase;

    cout << endl;
    cout << "-- HealingWarriorクラスの処理 --" << endl;

    // 治癒戦士クラスのインスタンスを生成する
    HealingWarrior* pHealer = new HealingWarrior();

    // 治癒戦士の気の出力を設定する
    pHealer->setPower(600);

    // 治癒戦士が修行する
    pHealer->train(2.0);

    // 治癒戦士の総修行ポイントを表示する
    cout << "総修行ポイント:" << pHealer->getTrainingPoint() << "ポイント" << endl;

    // 治癒戦士だけが持つ回復技を使う
    pHealer->healAlly();

    // 治癒戦士クラスのインスタンスを消去する
    delete pHealer;

    return 0;
}

実行結果

-- BaseWarriorクラスの処理 --
== BaseWarriorクラスのインスタンス生成 ==
気の出力400で1.5時間修行
600ポイント修行しました。
総修行ポイント:600ポイント
== BaseWarriorクラスのインスタンス消去 ==

-- HealingWarriorクラスの処理 --
== BaseWarriorクラスのインスタンス生成 ==
** HealingWarriorクラスのインスタンス生成 **
気の出力600で2時間修行
1200ポイント修行しました。
総修行ポイント:1200ポイント
仲間を回復する治癒の気を放つ
治癒部隊の呼び出しコードは777番
** HealingWarriorクラスのインスタンス消去 **
== BaseWarriorクラスのインスタンス消去 ==

利用できるメンバ関数の比較

BaseWarriorクラスとHealingWarriorクラスで利用できるメンバ関数を比較してみましょう。

メンバ関数BaseWarriorで利用HealingWarriorで利用
setPower
getPower
getTrainingPoint
train
healAlly×

BaseWarriorでは、基本的な修行機能だけを使えます。

HealingWarriorでは、BaseWarriorから受け継いだsetPower、getPower、getTrainingPoint、trainを使えます。
さらに、HealingWarrior独自のhealAllyも使えます。

ここが継承の大きなポイントです。

子クラスは、親クラスの機能を受け継ぎながら、自分だけの機能を追加できます。

メンバ変数の利用範囲

次に、メンバ変数の利用範囲を見てみましょう。

メンバ変数定義されているクラス直接利用できる場所
m_powerBaseWarriorBaseWarriorクラス内
m_trainingPointBaseWarriorBaseWarriorクラス内
m_healCodeHealingWarriorHealingWarriorクラス内

BaseWarriorのm_powerとm_trainingPointはprivateです。
そのため、BaseWarriorクラスの外から直接アクセスできません。

また、HealingWarriorはBaseWarriorを継承していますが、privateメンバ変数を直接操作することはできません。

たとえば、HealingWarriorの中から次のようにm_powerへ直接代入することはできません。

// privateメンバなので、子クラスから直接は使えない
// m_power = 600;

そのかわり、BaseWarriorのpublicメンバ関数であるsetPowerを使います。

pHealer->setPower(600);

このように、継承していてもprivateは守られます。
子クラスから利用できるのは、基本的には親クラスのpublicメンバ関数です。

protectedメンバの考え方

継承では、privateとpublicのほかに、protectedというアクセス指定も出てきます。

protectedは、クラスの外からは直接使えません。
しかし、子クラスからは使えるメンバです。

アクセス指定クラス外から子クラスから自分のクラス内から
public
protected×
private××

今回のサンプルでは、m_powerとm_trainingPointをprivateにしています。
そのため、HealingWarriorから直接触ることはできません。

これは、親クラスの大切な内部状態をむやみに子クラスへ触らせないためです。

ただし、今後、子クラスから親クラスの内部状態を使いたい場面が出てきたときには、protectedが使われることがあります。

ドラゴンボール風にたとえるなら、privateは基本戦士の体内にある完全に秘められた気の核です。
protectedは、弟子や派生流派だけが扱える秘伝の気脈のようなものです。
publicは、誰でも使える公開された修行コマンドです。

図:public、protected、privateの見え方

この図が示していること

この図では、継承時のpublic、protected、privateの見え方を表しています。

publicメンバは、クラス外からも子クラスからも使えます。
protectedメンバは、クラス外からは使えませんが、子クラスからは使えます。
privateメンバは、自分のクラス内からしか直接使えません。

今回のサンプルでは、親クラスのm_powerとm_trainingPointをprivateにしているため、HealingWarriorから直接触ることはできません。

BaseWarriorだけの処理の流れ

main.cppの前半では、BaseWarriorクラスのインスタンスを生成しています。

BaseWarrior* pBase = new BaseWarrior();

この時点でBaseWarriorのコンストラクタが呼び出されます。

== BaseWarriorクラスのインスタンス生成 ==

そのあと、setPowerで気の出力を400に設定します。

pBase->setPower(400);

次に、1.5時間修行します。

pBase->train(1.5);

train関数では、400 × 1.5 = 600ポイントが計算されます。

気の出力400で1.5時間修行
600ポイント修行しました。

最後にgetTrainingPointで総修行ポイントを表示し、deleteでインスタンスを消去します。

delete pBase;

このとき、BaseWarriorのデストラクタが呼び出されます。

== BaseWarriorクラスのインスタンス消去 ==

HealingWarriorの生成時に親クラスのコンストラクタも呼ばれる

main.cppの後半では、HealingWarriorクラスのインスタンスを生成しています。

HealingWarrior* pHealer = new HealingWarrior();

このとき、実行結果には次のように表示されます。

== BaseWarriorクラスのインスタンス生成 ==
** HealingWarriorクラスのインスタンス生成 **

ここで大切なのは、子クラスであるHealingWarriorのコンストラクタより前に、親クラスであるBaseWarriorのコンストラクタが呼び出されていることです。

つまり、子クラスのインスタンスが生成されるときは、次の順番でコンストラクタが呼び出されます。

親クラスのコンストラクタ → 子クラスのコンストラクタ

ドラゴンボール風にたとえるなら、治癒戦士が登場するとき、まず基本戦士としての体づくりが行われ、そのあと治癒戦士としての特別な能力が設定されるイメージです。

HealingWarriorは親クラスの機能を使える

HealingWarriorには、setPowerやtrainを直接定義していません。

しかし、次のように呼び出せます。

pHealer->setPower(600);
pHealer->train(2.0);

これは、HealingWarriorがBaseWarriorを継承しているからです。

BaseWarriorのpublicメンバ関数を受け継いでいるため、HealingWarriorでも同じように使えます。

2.0時間の修行では、600 × 2.0 = 1200ポイントになります。

気の出力600で2時間修行
1200ポイント修行しました。

さらに、HealingWarriorには独自のhealAllyがあります。

pHealer->healAlly();

実行すると、次のように表示されます。

仲間を回復する治癒の気を放つ
治癒部隊の呼び出しコードは777番

このhealAllyは、BaseWarriorにはない機能です。

つまり、HealingWarriorは、親クラスの基本機能に加えて、自分だけの機能を持っているということです。

子クラスの消去時はデストラクタの順番が逆になる

HealingWarriorインスタンスをdeleteすると、次のように表示されます。

** HealingWarriorクラスのインスタンス消去 **
== BaseWarriorクラスのインスタンス消去 ==

生成時は、親クラスから子クラスの順番でした。

BaseWarriorコンストラクタ → HealingWarriorコンストラクタ

しかし、消去時は逆になります。

HealingWarriorデストラクタ → BaseWarriorデストラクタ

これは、子クラスの部分を先に片付けてから、親クラスの部分を片付けるためです。

ドラゴンボール風に言えば、治癒戦士としての特別装備を先に解除し、そのあと基本戦士としての修行装置を停止するようなイメージです。

図:コンストラクタとデストラクタの呼び出し順

この図が示していること

この図では、子クラスのインスタンスが生成・消去されるときのコンストラクタとデストラクタの呼び出し順を表しています。

生成時は、まず親クラスのコンストラクタが呼ばれ、そのあと子クラスのコンストラクタが呼ばれます。

消去時は逆に、まず子クラスのデストラクタが呼ばれ、そのあと親クラスのデストラクタが呼ばれます。

この順番は、継承を理解するうえでとても重要です。

デストラクタにvirtualを付ける理由

今回のBaseWarriorクラスでは、デストラクタにvirtualを付けています。

virtual ~BaseWarrior();

HealingWarriorクラスのデストラクタにもvirtualを付けています。

virtual ~HealingWarrior();

C++では、継承を使うクラスの親クラスには、virtualデストラクタを用意することが推奨されます。

これは、今後ポリモーフィズムを使って、親クラス型のポインタで子クラスのインスタンスを扱う場面が出てくるためです。

たとえば、将来的には次のような形を使うことがあります。

BaseWarrior* p = new HealingWarrior();

このように、親クラス型のポインタで子クラスのインスタンスを指す書き方は、ポリモーフィズムにつながる大切な考え方です。

このとき、親クラスのデストラクタがvirtualでないと、deleteしたときに子クラス側のデストラクタが正しく呼ばれない可能性があります。

そのため、継承を前提にした親クラスでは、デストラクタにvirtualを付ける習慣が大切です。

virtual ~BaseWarrior();

ドラゴンボール風に言えば、親クラス型の通信装置から治癒戦士を呼び出していたとしても、退場時には治癒戦士としての装備まで正しく解除できるようにしておく、というイメージです。

継承とポリモーフィズムのつながり

今回のサンプルでは、まず継承の基本を確認しました。

BaseWarriorという親クラスを作り、HealingWarriorという子クラスがそれを受け継ぎました。

この継承の仕組みがあるからこそ、次に学ぶポリモーフィズムが成り立ちます。

ポリモーフィズムとは、同じ型や同じ呼び出し方を使いながら、実際のインスタンスの種類によって異なる動作をさせる考え方です。

ドラゴンボール風に言えば、司令官が全員に「技を出せ」と同じ指示を出しても、基本戦士は基本技、治癒戦士は回復技、惑星戦士は強力な気弾を出す、というようなイメージです。

今回の段階で大切なのは、次の3つです。

学習ポイント内容
継承親クラスの機能を子クラスが受け継ぐ
子クラスの拡張親クラスにない独自機能を追加できる
virtualデストラクタ将来のポリモーフィズムで安全に消去するために重要

今回のHealingWarriorは、BaseWarriorの修行機能を受け継ぎ、healAllyという独自機能を追加しました。

この形を理解しておくと、今後、virtual関数やオーバーライドを使ったポリモーフィズムの理解がしやすくなります。

継承で押さえたいポイント

継承を使うときは、次のポイントを押さえておきましょう。

ポイント内容
親クラス共通する機能をまとめる
子クラス親クラスの機能を受け継ぎ、独自機能を追加する
宣言方法class 子クラス名 : public 親クラス名
publicメンバ子クラスからも利用できる
privateメンバ子クラスから直接アクセスできない
protectedメンバ子クラスから利用できるが、クラス外からは使えない
コンストラクタの順番親クラス → 子クラス
デストラクタの順番子クラス → 親クラス
virtualデストラクタ継承を使う親クラスでは重要

C++の継承は、似たクラスの共通部分をまとめ、子クラスで個性を追加するための仕組みです。

BaseWarriorは、基本的な修行能力を持ちます。
HealingWarriorは、その能力を受け継ぎながら、仲間を回復する独自技を持ちます。

このように、共通する力を親クラスにまとめ、そこから派生した戦士たちが自分だけの技を持つ。
それが、C++における継承の大きな魅力です。

そして、その先にあるのがポリモーフィズムです。
継承を理解することで、同じ指示でも戦士ごとに異なる動きをさせる、より柔軟なオブジェクト指向の世界へ進めるようになります。