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

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

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

書き手:Hidano

前回はクラスの設計を行い、プレイヤーのオブジェクトを生成してテキスト形式のバトルをさせてみました。

オブジェクトについて1度で全て理解するのは難しいと思いますので、今回はシンプルなプログラムをいくつか比較して、早い段階で知っておきたい「スコープ」と「値渡し」について確認していきましょう。

まずは次の短いプログラムを書いてみます。

#include<stdio.h>

int main() {
    int year = 2018;
    if (year % 4 != 0) {
        int remain = year % 4;
    }
    printf("オリンピックまであと%d年!\n", remain);
    return 0;
}

最初に年数を入力し、4年に1度開催されるオリンピックまでの残り年数を計算しています。

残り年数はyearを4で割った余りで算出できるので、この場合の2018年の場合は、2018÷4 = 504あまり2 ということで、あと2年と表示して欲しいのですが、実際に開発環境に打ち込んでみると、このプログラムはコンパイルエラーになります。

f:id:NUT_SoftwareDevelopper:20161120012758j:plain

私のVisualStudioでも、printfでremainの下に赤い波線が引かれています。カーソルを当てると「識別子"remain"が定義されていません」と表示されます。

これはどうしてかというと、remainがif文の中括弧の中でで宣言されているからなんです。通常、中括弧内で宣言した変数は、中括弧内の処理が終わると同時に破棄されます

この変数の有効範囲のことを「スコープ」と呼びます。寿命のようなものです。変数は宣言されると同時に寿命が定義され、処理がスコープを抜けると同時に破棄されます。

※中括弧を使わない言語では、代わりにスペースやインデントでスコープを区切る場合があります。

remainはif文の内側で宣言されているので、if文内の処理が終わると同時に破棄されています。このため、mainのprintfから読み取れなかったんですね。

スコープ内で別のスコープが始まる入れ子構造になっている場合、外側のスコープで宣言された変数は内側のスコープ内でも生きています。まだ外側のスコープは抜けていませんからね。

というわけで、プログラムを正しく動作させるには主に次の方法が考えられます。

#include<stdio.h>

int main() {
    int year = 2018;
    //remainをif文の外側で宣言する
    int remain = 0;
    if (year % 4 != 0) {
        //こちらは「int 」を消す(消さないと同名の二重宣言になり、ややこしいことになる)
        remain = year % 4;
    }
    printf("オリンピックまであと%d年!\n", remain);
    return 0;
}

remainの宣言をif文の外側で行うことにより、スコープをmain全体に広げています。if文の処理はmainの中括弧の内側に定義されているため、main内で使える変数はif文の中でも使えるようになります。

もう一つの例です。

#include<stdio.h>

int main() {
    int year = 2018;
    if (year % 4 != 0) {
        int remain = year % 4;
        printf("オリンピックまであと%d年!\n", remain);
    }
    return 0;
}

remainを利用するprintfの方を、if文中に入れてしまいました。こうするとremainのスコープ内での使用になるので、エラーは発生しません。また、remainを使用する処理が一つの中括弧内で完結しているため、プログラム的にもちょっとスッキリしたかなと思います(個人的な感想です)。

どちらが正しいという事はありませんが、今回のような数行で用済みになってしまう変数なら、後者のようにスコープ内で完結させることで、プログラムを読み返したときに使う脳のリソースを稼げるでしょう。

コードの可読性について

プログラムというのはインプットとアウトプットさえ正しければ、途中の過程はどんなにゴチャゴチャしていても正しく動作します。

しかしプログラムを拡張したり、後から不具合修正をしたい時などに、以前書いたコードがどのような動作をしているのかパッと見て分からないと、とても辛い思いをすることになります。

なので、忘れた頃に読み返してすぐ処理の内容が理解できるよう、日頃から気を配っておくことが重要です。

今回の場合は、if文の中でしか使わない変数はif文を抜けると同時に破棄される=考慮しなくて良くなるので、逆にそういうことも考えながら読み進めると、素早く処理が理解できるようになるでしょう。

さて、スコープは関数の実行にも適用されますが、その挙動は少し異なります。次のプログラムを書いてみましょう。

#include<stdio.h>

void calc() {
    number = number * 3;
    printf("計算結果は%dです\n", number);
}

int main() {
    int number = 5;
    calc();
    printf("numberは%dです\n", number);
    return 0;
}

int型の引数を受け取ると、それに3を掛けて計算結果を表示する関数calcを作りました。

mainでは5を入れたint型の変数numberをcalcの引数に設定して計算させ、calc関数から抜け出たらnumberの値を表示します。

既に察した方も多いと思いますが、これもコンパイルエラーになります。calc内でnumberが定義されていないと警告されるはずです。

calcはmainの中で呼び出されているのですが、スコープはmainと並列の関係であり、入れ子構造にはなっていません。main内でcalcが実行されると、一時的にmainスコープから抜けてcalcスコープに処理が移り、calcのスコープを抜けたらまたmainスコープが再開されるという流れになっています。

そのため別のスコープで宣言されたnumberは、calc内で利用できないよということになります。

どうしてもcalc内にnumberの情報を持っていきたい時は引数を使えばよいのですが、ここでもちょっと問題が起こります。

#include<stdio.h>

void calc(int n) {
    n = n * 3;
    printf("計算結果は%dです\n", n);
}

int main() {
    int number = 5;
    calc(number);
    printf("numberは%dです\n", number);
    return 0;
}

ビルドするとどうでしょう。

f:id:NUT_SoftwareDevelopper:20161120005914j:plain

計算結果は正しいようですが、最終的なnumberは5から変化しませんでした。

ここでcalcに引数nを渡すことを「値渡し」といい、その名の通り変数numberの「5」という「値」だけを渡しているのです。

calc内では5という値を受け取って、さらにそれを3倍する処理を行い、結果も正しく表示されています。なのにmainに戻った時に反映されていないということは、これは値を受け取っただけで、calc内で完結してしまっているんですね。

そしてnumberとnは全く異なる変数であり、特に引数nのスコープはcalcの中括弧内なので、mainに処理が戻った段階で「15」という計算結果は引数nごとお亡くなりになっています。

では計算結果をmainに反映させるにはどうすれば良いのでしょう。これもいくつか方法があるのですが、最も分かりやすいのはこうでしょう。

#include<stdio.h>

//関数calcの型をintに変更
int calc(int n) {
    n = n * 3;
    printf("計算結果は%dです\n", n);
    //計算結果をreturnする
    return n;
}

int main() {
    int number = 5;
    int answer = calc(number);
    printf("answerは%dです\n", answer);
    return 0;
}

関数は返り値を渡すことが出来るので、データ型をintに変更し、nを返しています。

ちなみにここでも15という値が渡されているだけで、変数nはその後速やかに破棄されています。

しかし、こんな場合はどうでしょう。

#include<stdio.h>

int calc(int n1,double n2) {
    n1 *= 2;
    n2 *= 3;
    //return ???;
    return 0;
}

int main() {
    int num1 = 13;
    double num2 = 16.5;
    calc(num1, num2);
    printf("整数:%d 少数:%f\n", num1, num2);
    return 0;
}

calcはデータ型の異なる二つの数値を受け取り、整数は2倍、少数は3倍したいと思います。一体どこでこんな処理が必要になるのか分かりませんが、関数がreturnできる値は1つだけなので、両方の計算結果を返すことはできません。

これを上手く動かすには、本当の意味で引数に渡した変数そのものを操作する必要が出てきます。その為には、プログラムが動くコンピュータの仕組みについて少し理解しなければいけません。

というわけで次回は、メモリの概念について見ていきましょう。

クラスの変数のスコープ

前回クラスを設計した時に、クラス内に定義した関数でクラスの変数の値を変更していましたね。ちょっとその部分を抜粋してみます。

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;
        }
    }
};

クラスの変数であるhpを直接操作していますね。hpのスコープはクラス全体なので、関数Attackedの中でも操作が可能です。勿論、他に関数を増やしても可能です。

しかし、操作が可能なのは関数を実行しているオブジェクトの変数に限られるため、対戦相手のhpを直接削ることはできません。その為関数名も「Attacked」と受身の形にしました。相手の攻撃力は値を読むだけで十分ですからね。