【C++】継承とは?|protectedの使い方をゲームの敵クラスで初心者向けに解説

サムネイル画像 C++

今回は、C++での「継承」と「protected」について解説していきます。

「継承って何?クラスとどう違うの?」
「protectedってprivateとどう違うの?」
「virtualやoverrideって何のためにあるの?」

こんな疑問はありませんか?

継承は、前回のクラスの記事で作った Enemy クラスをさらに発展させる仕組みです。
「敵キャラの共通部分は親クラスにまとめて、種類ごとの違いは子クラスで書く」という設計が継承を使うとスッキリ実現できます。

この記事を読み終えると、あなたはC++の継承とprotectedの使い方をマスターできると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

継承とは?

継承とは、

  • 既存のクラスの機能を引き継いで、新しいクラスを作る仕組み

です。

引き継がれる元のクラスを基底クラス(親クラス)、引き継いで作った新しいクラスを派生クラス(子クラス)と呼びます。

たとえば、ゲームに「スライム」「ゴブリン」「ドラゴン」の3種類の敵がいるとします。
どの敵にも「HP」「ダメージを受ける処理」などの共通の機能があります。
これを毎回書くのは大変ですし、バグが増える原因になります。

そこで、共通の機能を Enemy(親)にまとめ、各敵(子)に引き継がせるのが継承です。

継承の基本の書き方

継承の書き方は以下の通りです。


class 派生クラス名 : public 基底クラス名 {
    // 追加・上書きしたいメンバ
};

実際に書いてみましょう。

▼enemy.h


#pragma once
#include <iostream>

// 基底クラス(親)
class Enemy {
public:
    Enemy(int hp) : m_hp(hp) {}
    virtual ~Enemy() {}  // 仮想デストラクタ(後述)

    void TakeDamage(int damage) {
        m_hp -= damage;
        if (m_hp < 0) m_hp = 0;
    }

    bool IsDead() const { return m_hp <= 0; }

    // 派生クラスで上書きできる関数(virtual)
    virtual void Attack() const {
        std::cout << "敵の攻撃!\n";
    }

protected:
    int m_hp;  // ← protected:派生クラスからアクセスできる
};

// 派生クラス①:スライム
class Slime : public Enemy {
public:
    Slime() : Enemy(30) {}  // 親のコンストラクタを呼ぶ

    void Attack() const override {  // 親の Attack() を上書き
        std::cout << "スライムが体当たり!(5ダメージ)\n";
    }
};

// 派生クラス②:ドラゴン
class Dragon : public Enemy {
public:
    Dragon() : Enemy(500) {}

    void Attack() const override {
        std::cout << "ドラゴンが炎を吐く!(80ダメージ)\n";
    }

    // ドラゴン固有の機能
    void Roar() const {
        std::cout << "ドラゴンが咆哮!HPは " << m_hp << "\n";
        // m_hp は protected なので派生クラスからアクセスできる
    }
};

▼main.cpp


#include "enemy.h"

int main() {
    Slime  slime;
    Dragon dragon;

    slime.Attack();
    dragon.Attack();
    dragon.Roar();

    dragon.TakeDamage(100); // 親の関数もそのまま使える
    std::cout << "ドラゴン死亡: " << (dragon.IsDead() ? "Yes" : "No") << "\n";

    return 0;
}

実行結果

スライムが体当たり!(5ダメージ)
ドラゴンが炎を吐く!(80ダメージ)
ドラゴンが咆哮!HPは 500
ドラゴン死亡: No

protectedとは?

先ほどのコードで m_hpprotected にしていました。
これが今回のポイントです。

protectedとは、

  • クラス内部と派生クラスからはアクセスできるが、外部からはアクセスできない

アクセス修飾子です。privatepublic の中間の存在です。

3つの違いを表にまとめます。

修飾子クラス内部派生クラスクラス外部
public
protected
private

m_hpprivate にしてしまうと、Dragon::Roar() の中で m_hp を読もうとしてもコンパイルエラーになります。
「派生クラスには使わせたいが、外からは触られたくない」というメンバには protected を使いましょう。

virtual(仮想関数)と override

継承と合わせて必ず覚えておきたいのが virtualoverride です。

  • virtual:親クラスの関数に付けることで、派生クラスで上書き(オーバーライド)できるようになる
  • override:派生クラス側に付けることで、「親の仮想関数を上書きしています」と明示できる(スペルミスなどを防ぐ安全装置)

virtual を付けない場合どうなるか、比べてみましょう。


// ❌ virtual なし(よくある失敗)
class Enemy {
public:
    void Attack() const { std::cout << "敵の攻撃!\n"; }
};

class Dragon : public Enemy {
public:
    void Attack() const { std::cout << "炎を吐く!\n"; }
};

int main() {
    Enemy* e = new Dragon();
    e->Attack(); // ← 「炎を吐く!」ではなく「敵の攻撃!」が呼ばれる!
    delete e;
    return 0;
}

実行結果

敵の攻撃!  ←  Dragon の Attack が呼ばれない!

virtual がないと、ポインタの型(Enemy*)を見て関数を呼ぶため、Dragonのものが無視されます
virtual を付けると、ポインタが指している実際のオブジェクトの型(Dragon)を見て呼び分けてくれます。これをポリモーフィズム(多態性)といいます。

仮想デストラクタを忘れずに

基底クラスのデストラクタには必ず virtual を付けてください。


// ❌ 仮想デストラクタなし(メモリリークの危険)
class Enemy {
public:
    ~Enemy() { std::cout << "Enemy 消えた\n"; }
};

// ✅ 仮想デストラクタあり(正しい)
class Enemy {
public:
    virtual ~Enemy() { std::cout << "Enemy 消えた\n"; }
};

Enemy*Dragondelete するとき、仮想デストラクタがないと Dragon のデストラクタが呼ばれません。基底クラスのデストラクタには必ず virtual を付けると覚えておきましょう。

【重要】私が実際にゲーム制作でハマった体験談

自主制作のRPGで敵キャラを継承で設計したとき、まんまとこの問題にハマりました。

Enemy を親にして SlimeDragon を作り、それぞれ Attack() を書いたのですが、Enemy* のポインタ配列に入れて呼び出すとどの敵も全員「敵の攻撃!」という親の処理しか実行しない状態になってしまいました。

30分以上悩んだ末に気づいたのは、親クラスの Attack()virtual を付け忘れていたことでした。


// ❌ 付け忘れていたコード
void Attack() const { std::cout << "敵の攻撃!\n"; }

// ✅ 直したコード
virtual void Attack() const { std::cout << "敵の攻撃!\n"; }

virtual の一語を足すだけで、全員がそれぞれの攻撃を正しく出すようになりました。
「派生クラスで上書きしたい関数には、必ず親側に virtual を付ける」を身に染みて覚えた出来事です。

よくある失敗例と対処法

  • 親クラスに virtual を付け忘れる
    → ポインタ経由で呼ぶと、派生クラスの関数が無視されて親の関数が呼ばれ続けます。オーバーライドしたい関数には必ず親側に virtual を付けましょう。

  • override を付け忘れてスペルミスに気づかない
    Atack() のようにスペルを間違えても、override なしではコンパイルエラーになりません。派生クラス側には必ず override を付けて、意図通りの上書きかコンパイラに確認させましょう。

  • 基底クラスのデストラクタに virtual を付け忘れる
    Enemy*delete したとき、派生クラスのデストラクタが呼ばれずにメモリリークが起きます。基底クラスのデストラクタには必ず virtual ~クラス名() と書きましょう。

発展:純粋仮想関数(抽象クラス)

仮想関数に = 0 を付けると純粋仮想関数になります。純粋仮想関数を持つクラスは抽象クラスと呼ばれ、直接インスタンスを作れません。


class Enemy {
public:
    virtual ~Enemy() {}

    // 純粋仮想関数:派生クラスで必ずオーバーライドしなければならない
    virtual void Attack() const = 0;

protected:
    int m_hp = 0;
};

// Enemy enemy; ← ❌ コンパイルエラー(抽象クラスは直接生成不可)
// Slime slime; ← ✅ Attack() を override していれば生成できる

すべての派生クラスに必ず実装させたい関数」には純粋仮想関数を使うと、実装漏れをコンパイルエラーで検出できます。

継承とデザインパターンを組み合わせた実践的な設計については、デザインパターンをゲーム開発で活用する記事ステートパターンの記事もあわせて読んでみてください。

まとめ

  • 継承:既存クラス(親)の機能を引き継いで新しいクラス(子)を作る仕組み
  • protected:クラス内部と派生クラスからはアクセスできるが、外部からはアクセスできない
  • private との違い:private は派生クラスからもアクセス不可、protected は派生クラスからアクセスできる
  • virtual:親の関数に付けることで派生クラスでオーバーライドできるようになる
  • override:派生クラス側に付けることで、意図通りの上書きかコンパイラが確認してくれる
  • 基底クラスのデストラクタには必ず virtual を付ける
  • 純粋仮想関数(= 0):派生クラスに実装を強制できる

これでC++の継承とprotectedの使い方はバッチリですね!

ここまで読んでくださり、ありがとうございました。
この記事が皆様の学習の役に立てば幸いです。

関連記事