読者です 読者をやめる 読者になる 読者になる

ここまでの冒険を記録しますか?

長岡技大ソフトウェア開発サークルの開発ブログ

C++で覚えるプログラミングのイロハ【ニ】

書き手:Hidano

一体いつから――イロハの【ハ】で終わりと錯覚していた……?

というわけで、プログラミングの基本について、引き続き解説していきます。

オブジェクト指向という言葉を聞いたことがあるでしょうか。これは規模の大きいプログラムの動作を直感的に理解しやすくするために、役割ごとに分割された「オブジェクト」同士のやり取りで処理をさせる方式を指します。

反対に、オブジェクトを用いずにソースコードの上から順に処理していく方式は「手続き型」と呼ばれます。

C++の原型であるC言語手続き型言語ですが、C++はそこにオブジェクト指向を取り入れたことで、登場から30年以上経った現在でも広く利用される利便性を確立したのでしょう。

そんなわけで、今回はオブジェクト指向になくてはならない「クラス」と「オブジェクト」について見ていきます。

まずクラスとはなんぞやというところですね。

int型の変数には整数値を1つ保管でき、char型は1文字保管できます。それに対してクラスは、複数の変数を束ねて一つのデータ型として扱えます。

クラスはclassというキーワードを用いて、以下のように定義します。

class Player {
public:
    int hp;
    int attack;
    int defence;
};

ここではint型の値を三つ格納できる「Player」という名前のクラスを作成し、そこに体力、攻撃力、防御力の3つのパラメータを格納できるようにしています。頭に書いてある「public:」は、「これらの変数は誰でも参照できるよ」という意味で、他に「private」「protected」などがあります。これを使い分けることで思わぬエラーを防止できるのですが、最初の内は思考停止で「public:」だけを使ってよいかと思います。

さて、このクラスを元にプレイヤーのオブジェクトを作り、簡単な対戦をさせてみましょう。

この記事で確実に覚えていただきたいのは、「『クラス』と『オブジェクト』は本質的に異なる」ということです。

クラスはいわば「オブジェクトの設計図」であり、クラスを定義しただけではプログラムには基本影響を与えません。関数とも違って、プログラムを動かしてもhp,attack,defence分の変数が作られることもありません。

main関数内でプレイヤーを作り出すには、そのクラスの型を持つ変数を宣言します。この変数がすなわち「オブジェクト」というわけです。

先ほどのクラスの定義に続いて、main関数を書いてプログラムを完成させましょう。

#include<stdio.h>

class Player {
public:
    int hp;
    int attack;
    int defence;
};

int main() {
    
    //Player型のオブジェクトを生成
    Player playerA;
    //オブジェクトのパラメータには「.」でアクセス
    playerA.hp = 100;
    playerA.attack = 30;
    playerA.defence = 10;

    //オブジェクト生成時にまとめて定義することも可能
    Player playerB = { 120, 20, 15 };

    printf("playerA(HP:100 AT:30 DF:10) vs playerB(HP:120 AT:20 DF:15)\nEnterキーで次へ\n");

    //キー入力受付
    getchar();

    while (true) {
        printf("playerAの攻撃! playerBに%dのダメージ!\n", playerA.attack - playerB.defence);
        //ダメージ除算
        playerB.hp -= playerA.attack - playerB.defence;
        printf("playerBの残りHP:%d\n", playerB.hp);
        //生存判定
        if (playerB.hp <= 0) {
            printf("playerBの負け!\n");
            break;
        }
        getchar();

        printf("playerBの攻撃! playerAに%dのダメージ!\n", playerB.attack - playerA.defence);
        playerA.hp -= playerB.attack - playerA.defence;
        printf("playerAの残りHP:%d\n", playerA.hp);
        if (playerA.hp <= 0) {
            printf("playerAの負け!\n");
            break;
        }
        getchar();
    }

    return 0;
}

戦闘自体は単純で、攻撃プレイヤーのattackから防御プレイヤーのdefenceを引いてダメージ値を算出し、その値を防御プレイヤーのHPから引いています。先にHPが0以下になった方が負けです。

重要なのはwhile文より上の処理です。

PlayerクラスからplayerA,playerBの二つのオブジェクトを生成しました。

playerAとplayerBはそれぞれhp,attack,defenceの変数を持っており、別々の値を格納させられます。

クラスを使わない場合、main関数内でこんな変数を作らなければいけません。

int playerAHp = 100;
int playerAAtack = 30;
int playerADefenxe = 10;

int playerBHp = 120;
int playerBAtack = 20;
int playerBDefenxe = 15;

ちょっと煩わしいですね。今回はパラメーターが3種類、オブジェクトは2つしか作らないのでまだ大丈夫ですが、これで「素早さ」「回避率」等どんどんパラメーターを増やせば、それに比例して変数を山ほど作らなければならなくなります。

しかしクラスを使えば、その定義内にパラメータを追加することで、そのクラスから生まれるオブジェクトで新しいパラメータを使うことができるようになります。

ゲームには様々なキャラクターが登場し、それぞれ異なるパラメータを持っています。これを全て単体の変数で管理するのはとても現実的ではないので、クラスの変数とすることで「入れ子構造」にする必要があるのです。

さて、main関数の説明に戻りましょう。playerAを作り出しただけでは、HPや攻撃力といった能力値が設定されていません。オブジェクトが持つ変数を扱うには「オブジェクト名.変数名」と書くことで代入や参照ができるようになります。これを「(オブジェクトの変数に)アクセスする」と言います。

また、配列のように「{}」でパラメータを一気に設定することもできます。パラメータの種類が少ない時はこのように書いたほうがスッキリしますね。

「getchar()」は「キー入力を受け付ける」関数なのですが、ここでは「処理を一時停止させる」為に使います。なので実行時はEnterキーを押して次へ進むようにしてください。

while文の中身は変数がクラスのパラメータになっている以外は通常の演算と変わりません。

強いてひとつだけ、「break;」というのが新しい制御文です。「壊す」を意味する単語ですが、for文やwhile文のループ内で「break;」の処理に当たると、それ以降の処理をすっとばしてループから脱出します。非常によく使う処理なので覚えておきましょう。

「複数のデータをまとめる」というクラスの利用目的については理解していただけたでしょうか。しかし、クラスにはまだ便利な機能があります。それは、「オブジェクトごとに関数を扱える」ことです。

先ほどのクラスに関数を追加して、main内での処理を簡略化してみました。

#include<stdio.h>

class Player {
public:
    int hp;
    int attack;
    int defence;
    //キャラクター名
    char *name;

    bool Attacked(Player enemy) {
        printf("%sの攻撃! %s%dのダメージ!\n", enemy.name, name, enemy.attack - defence);
        hp -= enemy.attack - defence;
        printf("%sの残りHP:%d\n", name, hp);
        if (hp <= 0) {
            printf("%sの負け!\n",name);
            return false;
        }
        else {
            return true;
        }
    }
};

int main() {
    
    Player playerA = { 100, 30, 10, "フジキド" };
    Player playerB = { 120, 20, 15, "ヤモト"};
    printf("%s(HP:%d AT:%d DF:%d) vs %s(HP:%d AT:%d DF:%d)\nEnterキーで次へ\n", playerA.name, playerA.hp, playerA.attack, playerA.defence, playerB.name, playerB.hp, playerB.attack, playerB.defence);

    getchar();

    while (true) {
        bool result = playerB.Attacked(playerA);
        getchar();
        if (!result) {
            break;
        }
        result = playerA.Attacked(playerB);
        getchar();
        if (!result) {
            break;
        }
    }

    return 0;
}

先ほどの例ではmain関数に戦闘処理を書いていましたが、クラスの関数にすることでmain関数内に二人分の処理を書く必要がなくなりました。およそ10行分、コードが節約できました。

今回は二人で対戦するゲームですが、もっと戦闘キャラクターが増えるRPGでは、各戦闘は関数にして対戦相手を引数で指定するようにしないと、ソースコードが膨大になってしまいますね。

オブジェクトが持つ関数にアクセスするときも、変数と同様に「.」の後に関数名を指定します。

またnameパラメータを追加したことで、オブジェクトを生成した時に1回名前を設定すれば、その名前がその後ずっと使えるようになっています。

ところで、今回クラス内で定義したAttacked関数ですが、クラスの関数ではなくて普通の関数としてmainの真上にでも定義すれば、それで十分使い回しがきくようになるのではと疑問に思った方もいるのではないでしょうか。

実は、普通に上記の関数の場所をコピペするだけでは、正しく計算してくれないんです。色々工夫すれば正しく動くのですが、そこまでするならクラス内で定義した方が面倒でないという話になります。

では何故普通には動いてくれないのか。次回はその辺りの約束事について見ていきましょう。