C++入門|C++のオーバーライド

親から受け継いだ技を、そのまま使うだけじゃ終わらない。オーバーライドは、子クラスが自分らしい戦い方に進化させるための仕組みです。

C++の継承を学んでくると、親クラスの機能を子クラスが受け継げることが分かってきます。
でも、実際のプログラムでは「受け継いだ機能をそのまま使う」だけでは足りない場面がよくあります。

ドラゴンボールの世界で考えると、どの戦士にも「移動する」という共通の行動はあります。
ただし、移動のしかたはみんな同じではありません。

地上を駆け抜ける戦士もいれば、舞空術で空を高速移動する戦士もいます。
つまり、行動の名前は同じでも、中身の動きは違ってよいわけです。

C++では、このように親クラスにあるメンバ関数を、子クラス側で自分用の内容に作り直すことができます。
これがオーバーライドです。

今回は、ドラゴンボール風の世界観に合わせて、

  • 基本の移動を行う Warrior クラス
  • Warrior を継承し、移動方法を自分流に変える FlyingWarrior クラス

を使って、オーバーライドの考え方をじっくり見ていきます。

オーバーライドは「同じ技名で中身を変える」仕組み

オーバーライドをひとことでいうと、親クラスにある関数を、子クラスで上書きすることです。

今回のイメージでは、親クラス Warrior に move 関数があります。
この move は「戦士が移動する」という共通の動作を表します。

ところが、空を飛べる戦士 FlyingWarrior では、ただ地上を走るだけでは物足りません。
そこで、FlyingWarrior クラスにも同じ名前の move 関数を用意し、その中身を「舞空術で空を高速移動する」という内容にします。

これによって、

  • Warrior の move は地上移動
  • FlyingWarrior の move は空中移動

というように、同じ move でもクラスごとに違うふるまいを持たせられます。

今回のサンプルで登場するクラス

クラス名親クラス役割ファイル
Warriorなし基本の戦士warrior.h、warrior.cpp
FlyingWarriorWarrior空を飛ぶ戦士flyingWarrior.h、flyingWarrior.cpp

図:オーバーライドの基本イメージ

この図が示していること

この図では、親クラス Warrior が持つ move 関数を、子クラス FlyingWarrior が同じ名前で持ちながら、中身だけ変えていることを表しています。

関数名は同じ move ですが、表示内容は違います。
これがオーバーライドのいちばん基本の形です。

サンプルプログラムを見てみよう

それでは、実際のプログラムを見ていきましょう。

親クラス Warrior を定義する

まずは親クラスです。
基本となる戦士クラス Warrior に、move 関数を用意します。

プロジェクト/ファイル名: Chap5_05/warrior.h

#ifndef _WARRIOR_H_
#define _WARRIOR_H_

class Warrior {
public:
    // 戦士が移動する
    void move();
};

#endif // _WARRIOR_H_

プロジェクト/ファイル名: Chap5_05/warrior.cpp

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

using namespace std;

// 戦士が移動する
void Warrior::move() {
    cout << "気をまとって地上を走る" << endl;
}

この Warrior クラスは、とてもシンプルです。
public なメンバ関数として move を1つ持っています。

move が呼び出されると、

気をまとって地上を走る

と表示されます。

ここでは「基本の戦士はこう移動する」という、親クラスらしい共通動作を表しています。

子クラス FlyingWarrior を定義する

次に、Warrior を継承した FlyingWarrior クラスを作ります。

FlyingWarrior は「空を飛べる戦士」なので、move の内容を自分用に変えます。

プロジェクト/ファイル名: Chap5_05/flyingWarrior.h

#ifndef _FLYING_WARRIOR_H_
#define _FLYING_WARRIOR_H_

#include "warrior.h"

class FlyingWarrior : public Warrior {
public:
    // オーバーライドした移動関数
    void move();
};

#endif // _FLYING_WARRIOR_H_

プロジェクト/ファイル名: Chap5_05/flyingWarrior.cpp

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

using namespace std;

// 空を飛べる戦士の移動
void FlyingWarrior::move() {
    cout << "舞空術で空を高速移動する" << endl;
}

ここで注目したいのは、FlyingWarrior クラスにも move 関数があることです。

親クラス Warrior にも move がありました。
子クラス FlyingWarrior にも move があります。

しかも、

  • 関数名は同じ
  • 戻り値の型は同じ
  • 引数も同じ

になっています。

このような形で、子クラス側に同じ関数を定義すると、親クラスの関数を子クラス側の内容で使うことになります。
これがオーバーライドです。

main 関数で動きを確認する

最後に main.cpp で、親クラスと子クラスのインスタンスをそれぞれ生成して、move を呼び出してみます。

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

#include "warrior.h"
#include "flyingWarrior.h"
#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    // 基本の戦士を生成する
    Warrior* pWarrior = new Warrior();

    // 空を飛べる戦士を生成する
    FlyingWarrior* pFlyingWarrior = new FlyingWarrior();

    // それぞれ移動させる
    pWarrior->move();
    pFlyingWarrior->move();

    // 使い終わったインスタンスを消去する
    delete pWarrior;
    delete pFlyingWarrior;

    return 0;
}

実行結果

気をまとって地上を走る
舞空術で空を高速移動する

実行結果を見ると、とても分かりやすいですね。

同じ move という関数を呼び出しているのに、

  • Warrior の move は地上移動
  • FlyingWarrior の move は空中移動

と、クラスによって結果が変わっています。

実行結果から分かるオーバーライドの意味

このサンプルの大事なところは、同じ move という名前の関数でも、どのクラスのインスタンスかによって実行される内容が違うという点です。

pWarrior->move();

こちらは Warrior クラスの move が実行されます。

pFlyingWarrior->move();

こちらは FlyingWarrior クラスの move が実行されます。

親クラスに move があるからといって、子クラスで同じ move を定義したのに親の move が使われるわけではありません。
子クラスでは、子クラス側に定義された関数が優先されるのです。

図:同じ move でも実行結果が変わる

この図が示していること

この図では、同じ move 関数を呼び出していても、インスタンスの型が違えば実行結果も変わることを表しています。

親クラスの戦士は地上を走り、子クラスの戦士は舞空術で飛びます。
これが「子クラスで関数の中身を変える」オーバーライドの分かりやすい例です。

オーバーライドが成立する条件

オーバーライドとして考えるときは、親クラスと子クラスの関数が、次のような関係になっていることが大切です。

項目親クラス子クラス
関数名movemove
戻り値の型voidvoid
引数なしなし

つまり、

  • 関数名が同じ
  • 戻り値の型が同じ
  • 引数の数や型も同じ

という形になっています。

今回の例でいうと、親クラスと子クラスの両方にある関数は、どちらも次の形です。

void move();

このように、まったく同じ形の関数を子クラス側で定義することで、親クラスの関数を子クラス用のふるまいに変えられます。

どこが「上書き」なのかを整理しよう

オーバーライドという言葉には、「上書きする」という意味があります。

今回のサンプルでは、Warrior クラスにある move が土台になっています。
そして FlyingWarrior クラスでは、その move を空を飛ぶ戦士向けの内容に作り直しています。

イメージとしては、こんな感じです。

  • Warrior の move
    → 気をまとって地上を走る
  • FlyingWarrior の move
    → 舞空術で空を高速移動する

つまり、「移動する」という大枠は親クラスから受け継ぎつつ、具体的な動き方だけを子クラスで自分流に変えているわけです。

ドラゴンボールの世界でいえば、同じ「移動」という技名でも、戦士ごとに戦い方が違うので、中身をそれぞれの流派に合わせて調整しているイメージです。

プログラムの流れを順番に追ってみよう

ここで、main 関数の流れを順番に確認してみましょう。

基本の戦士を生成する

Warrior* pWarrior = new Warrior();

ここで Warrior クラスのインスタンスが生成されます。
pWarrior は Warrior 型のインスタンスを指すポインタです。

空を飛べる戦士を生成する

FlyingWarrior* pFlyingWarrior = new FlyingWarrior();

ここで FlyingWarrior クラスのインスタンスが生成されます。
pFlyingWarrior は FlyingWarrior 型のインスタンスを指すポインタです。

親クラスの move を呼び出す

pWarrior->move();

これは Warrior クラスの move を実行するので、

気をまとって地上を走る

と表示されます。

子クラスの move を呼び出す

pFlyingWarrior->move();

こちらは FlyingWarrior クラスの move を実行するので、

舞空術で空を高速移動する

と表示されます。

このように、同じ move でも、どのクラスのインスタンスに対して呼び出したかで中身が変わります。

オーバーライドを使うと何がうれしいのか

オーバーライドの良いところは、同じ名前の関数で共通の意味を保ちながら、クラスごとに違う動作を実現できることです。

もしオーバーライドを使わずに全部別の名前にしてしまうと、

  • Warrior なら runOnGround
  • FlyingWarrior なら flyInSky

のように、関数名がバラバラになってしまいます。

これだと、「どちらも移動の処理なんだな」という統一感が弱くなります。

でも、どちらも move にしておけば、「移動する」という意味は共通だとすぐ分かります。
そのうえで、具体的な動き方だけをクラスごとに変えられます。

これはオブジェクト指向らしい、とてもきれいな設計です。

オーバーライドのイメージをもっと整理する

親クラスは「共通の骨組み」を持っています。
子クラスは、その骨組みを受け継いだうえで、「ここは自分用に変えたい」という部分だけを作り直します。

今回だと、

  • 親クラス Warrior
    → 戦士は move できる
  • 子クラス FlyingWarrior
    → 戦士は move できる。ただし空を飛んで移動する

という関係です。

この考え方はとても大切で、後でたくさんの子クラスが増えても、「移動は move」という共通ルールを保ったまま拡張していけます。

たとえば、さらに別の戦士クラスを追加して、

  • HeavyWarrior は重装備で突進する
  • ShadowWarrior は残像を残して移動する
  • TeleportWarrior は瞬間移動する

というふうに、それぞれが move を持てば、世界観を壊さずにクラスを増やしていけます。

図:親の move を子が自分流に作り直す

この図が示していること

この図では、親クラスが持つ move という共通の行動を、複数の子クラスがそれぞれ自分のスタイルに合わせて作り直せることを表しています。

オーバーライドは、単に関数を上書きするだけでなく、共通の意味を保ちながら個性を持たせるための仕組みだと分かります。

このサンプルで押さえておきたいポイント

今回のオーバーライドのサンプルで、特に大切なのは次の点です。

ポイント内容
親クラスにも子クラスにも同じ名前の関数がある今回は move
戻り値の型と引数も同じどちらも void move()
子クラスの関数が親クラスの内容を自分用に変える地上移動から空中移動へ
実行結果がクラスごとに変わるWarrior と FlyingWarrior で表示が違う

C++のオーバーライドは、継承を学んだあとにぜひしっかり身につけたい大事な仕組みです。

親クラスの機能をただ受け継ぐだけではなく、子クラスらしいふるまいに育てていくことができるからです。

ドラゴンボール風にいうなら、師匠から受け継いだ基本技を、そのまま使うだけではなく、自分の流派や戦い方に合わせて進化させるようなものです。

Warrior が move を持っている。
FlyingWarrior も move を持っている。
でも、その中身は同じではない。

この「同じ名前で、違うふるまい」を作れるようになると、オブジェクト指向の面白さがぐっと見えてきます。