ポケモンで学ぶC++オブジェクト指向入門

以前妻にc++のクラスの使い方を教えたときに、ブログ記事にしたらいいのに、と言われたので公開しておく。

対象者:intとかdoubleの変数やその配列を扱え、関数を作ったり使ったりできる。ポインタも一応わかる。でもclassの使い方がわからない。そんな人。

記事を読むと?: c++でif else分岐の多かったコードを少し見通しよくできるようになる。 (c++で継承を利用したポリモーフィズムなプログラムを書けるようになる)

本題:

あなたはポケモンを実装しているプログラマです。 ポケモンバトルにおける"こうげき処理"をc++で実装しようとしています。 「プレイヤーがてもちのポケモンから一匹えらんで、あいてのポケモンをこうげきする」という処理について考えています。 ここでは、ポケモンは一つのわざしか覚えていないと仮定します。 一旦c++の正しい文法は無視して、実直に書くとこうなります。

ポケモン = ポケモンを選ぶ();
if (ポケモン == ピカチュウ) {
    十万ボルト(ポケモン);
} else if (ポケモン == ゼニガメ) {
    ハイドロポンプ(ポケモン);
} else {
    ...
}

ポケモンの数だけif elseが続きますね。 ポケモンが増えるごとにこの処理を変更しないといけません。 大変です。

そこで、オブジェクト指向プログラミング(Object Oriented Programming)です。 以下のように考えます。こちらも一旦正しいc++の文法を無視します。

// 基底クラス
class ポケモン {
    void こうげき();
};

// ポケモンクラスを"継承"したピカチュウクラス
class ピカチュウ : ポケモン {
    void こうげき() override {
        十万ボルト();
    }
    void 十万ボルト() {
        // 十万ボルトの実装
    }
};

class ゼニガメ : ポケモン {
    void こうげき() override {
        ハイドロポンプ();
    }
    void ハイドロポンプ() {
        // ハイドロポンプの実装
    }
};

まず、「ポケモン」というクラスを作ります。 これはポケモンがどういうことができるか、という特性を示してくれます。 ここではポケモンは「void こうげき()」関数を持っていることがわかります。

そしてポケモンクラスの子供にあたる「ピカチュウ」クラスを作ります。 ピカチュウポケモンなので自然です*1。 これを基底クラスを継承して派生クラス(子クラス)を作る、といいます。 そしてピカチュウクラスの中で、ピカチュウは実際にどのようなこうげきをするのかを具体的に記述します。 ピカチュウは十万ボルトが使えるので十万ボルトについて書きましょう。 ゼニガメについても同様です。

ピカチュウクラスは、あくまでピカチュウという種族についての書いているだけです。 「サトシのピカチュウ」、というある個体を表現するにはこうです。

ピカチュウ  サトシのピカチュウ();

この「サトシのピカチュウ」のことをピカチュウクラスの「インスタンス」と呼びます。

int i(0);

のようにintの変数を宣言するのと同じことです。 intがピカチュウになっただけです。

ポケモンがこうげきするときはこのように書きます。

サトシのピカチュウ.こうげき();

サトシのピカチュウへのポインタを知っているときには"->"を使って書きます。

サトシのピカチュウ->こうげき();
// 以下と同じ
(*サトシのピカチュウ).こうげき();

さて、「てもちのポケモンから一匹えらんで、あいてのポケモンをこうげきする」という処理はどのように書けるでしょうか。

ポケモン* 手持ちのポケモン[2];
ピカチュウ  サトシのピカチュウ();
ゼニガメ サトシのゼニガメ();
手持ちのポケモン[0] = &サトシのピカチュウ;
手持ちのポケモン[1] = &サトシのゼニガメ;

int i = ポケモンを選ぶ();
手持ちのポケモン[i]->こうげき();

if elseを使わなくて済み、 ポケモンの種類が増えた場合、例えばミュウツーが生み出された場合には、 ポケモンクラスを継承したミュウツークラスを作るだけです。べんりですね!

さて、c++を使って正しい文法で実装してみましょう。

#include <iostream>
 
class Pokemon {
public:
    // コンストラクタ
    Pokemon(const std::string& nickname) {
        this->nickname_ = nickname;
        // nickname_ = nickname; と省略できる
    }
    // デストラクタ
    virtual ~Pokemon() {
    }
    virtual void attack() = 0;
protected:
    std::string nickname_;
};

class Pikachu : public Pokemon {
public:
    Pikachu(const std::string& nickname) : Pokemon(nickname) {
    }
    virtual ~Pikachu() {
    }
    void attack() override {
        std::cout << nickname_  << "の十万ボルト!" << std::endl;
    }
};

class Zenigame : public Pokemon {
public:
    Zenigame(const std::string& nickname) : Pokemon(nickname) {
    }
    virtual ~Zenigame() {
    }
    void attack() override {
        std::cout << nickname_ << "のハイドロポンプ!" << std::endl;
    }
};


int main() {
    int i;
    Pokemon* monsters[2];
    monsters[0] = new Pikachu("サトシのピカチュウ");
    monsters[1] = new Zenigame("サトシのゼニガメ");
    std::cin >> i;
    if (i >=0 && i < 2) {
        monsters[i]->attack();
    }
    delete monsters[0];
    delete monsters[1];
}

なんだか色々増えましたね。

まず「コンストラクタ」と「デストラクタ」はそれぞれそのインスタンスを作ったときと消えるときに呼ばれる関数です。 ポケモンの個体にはニックネームがあるので、そのポケモン個体を宣言し生み出す時に同時にニックネームも設定したいですね? まず、ポケモンクラスの中にニックネームを格納する部分を作ります。 それがstd::string nickname_;の部分で、これをクラスメンバ変数と言います。 Pickachu("サトシのピカチュウ");ピカチュウクラスに渡されたニックネームが巡り巡って、 this->nickname_ = nickname;の部分でnickname_に設定されます。 thisというのは今作ったり消したりしようとしているポケモン個体(インスタンス)へのポインタです。 各個体ごとに別のニックネームが設定されることになります。

    virtual void attack() = 0;

virtual なんとか = 0;は純粋仮想関数といいます。継承先(ピカチュウ等各ポケモンクラス)でちゃんと実装してくださいね〜でないと空っぽのままですからね〜と指示しています。

    new Pikachu("サトシのピカチュウ");
    delete pikachu;

new/deleteはご存知でしょうか?まぁPikachu satoshi_pikachu();と同じようなもの*2ですが、newはポケモン個体を作った後ポインタが帰ってきます。ポインタの先の実体はスコープを抜けても終了処理が行われずメモリリークするので、使わなくなったらdeleteをしなければなりません。普通はnew/deleteを使わずshared_ptrやunique_ptrを使います。気になったら調べてみてください。

また、publicやprotectedという言葉が出てきました。他にもprivateがあります。 これはアクセス指定子といってその変数やクラスが内外のスコープからアクセスできるかどうかを示しています。

  • publicは、どこからでもアクセスできます。
  • protectedだと継承先のクラスでは見えますが(main関数など)外からは見えません。
  • privateだと宣言したクラスでしかアクセスできません。

という意味になります。

class Zenigame : public Pokemon {

この部分のpublicについての解説は省略します。大抵の場合でpublicを用います。

実行してみましょう。

$ g++ pokemon.cpp -o pokemon
$ ./pokemon
0
サトシのピカチュウの十万ボルト!
$ ./pokemon
1
サトシのゼニガメのハイドロポンプ!

いいですね!

*1:これらはis-aの関係にあります。 ja.wikipedia.org

*2:いいえ、全然違います。普通のsatoshi_pikachuはスタック領域に存在しますが、newで作られたピカチュウはヒープ領域にいます。 メモリとスタックとヒープとプログラミング言語 | κeenのHappy Hacκing Blog