next up previous contents
Next: 5. 課題 20 Up: 20. 構造体 III Previous: 3. 構造体の配列の初期化

4. 関数と構造体

関数の返り値は一つであり、関数の返り値を用いて値をやり取りする限りでは、通常は いくつもの値を受け取る事は出来ないが、構造体を使えば返り値に幾つもの値を持た せる事が出来る。しかし、その時には、何を返り値にするべきなのか、従って、関数 はどういう設計をすればよいのか、という設計指針の問題が生じる。構造体によって 幾つもの値を関数の返り値に出来るからと言って、行き当たりばったりに関数を作成 してはならない。あくまでも関数は機能本位に、どういう値に対して何をするものか を良く考えて作成しなければならない。

 

ここでは、ロールプレイングゲームを例に取って考えよう。 ロールプレイングゲームでは、まず hero がいる。 hero は、 色々なところを歩き回るので、位置をデータに持っている必要がある。 また、 hero が成長していくのならば、そのレベルを記録しておかねば ならない。ところが、レベルと言っても、攻撃力や守備力、魔法が使えるならば 魔法力、体力など様々な値が必要である。更に、 hero は所持品を持ってい る場合もあるだろう。

例えば、 hero の位置は、(x,y) 座標で、レベルは体力、攻撃力、守備力 があり、所持品は剣と鎧を持てるとすると、 hero を表す構造体は、次の ように書ける。


例 7
        struct Position { int x,y;};   /* 位置 */
        struct Lebel {          /* レベル */
                int point;      /* 得点 */
                int life_upper; /* 体力の上限 */
                int life;       /* 体力 */
                int offence;    /* 攻撃力 */
                int defence;    /* 守備力 */
        };
        struct Item {   /* 所持品 */
                int drug;       /* 薬の量 */
                int sword;      /* 剣の強さ */
                int buckler;    /* 盾の強さ */
                int armor;      /* 鎧の強さ */
        };

        struct Hero {
                struct Position position;
                struct Lebel lebel;
                struct Item item;
        };

一方、 hero が薬を使う場合は、そのための関数を 作成し、その関数の中で、体力を回復させることになる。この場合、 例えば、関数の型、引数は次のようになる。


例 8
        struct Hero use_drug(struct Hero hero){
                ...
                hero.item.drug=0;
                return hero;
        }

この場合は、hero.level.life の変更と hero.item.drug の消費という 2 つのデータに関係し、返り値も少なくとも この 2 つを返さなければならないので、struct Hero 型の構造体変数を 返している。

このように、関数の引数と返り値を考えるときは、その関数内で変更を受ける データに着目するようにすると良い。

一方、プレイヤーの生死の判断や、敵との遭遇の判断等は結果が明らかに真偽で あらわせられる場合は、関数の返り値は構造体にせずに真又は偽にしておいた方が 良いだろう。この場合、敵との戦闘などプレイヤーや敵のデータに変更 があり、なおかつ関数の返り値も欲しい ( 戦闘が終わったかどうか等 )という 場合でから、通常解決方法は 2つある。

後者の方法を使ったプログラムだと例えば次のようになる。


例 9
        int fight(struct Hero *phero, struct Enemy *penemy){
                ...
                dum = (phero->level.offence*(rand()%10)) 
                    / (penemy->level.defence + 1);
                penemy->level.life -= dum;    /* 敵へのダメージ */
                ...
                return 0;	/* 戦闘が続いている時 */
                ...
                return 1;	/* 戦闘が終わった時 */
        }

ここで注意するのは、ポインタを使用しているために、関数 fight() 内で 行ったダメージを受けた敵の体力の変更は、この関数に渡された構造体変数に たいして直接行っているために、関数が終了しても有効な点である。つまり、 ポインタを使う事で、関数の中から直接外部のデータを書き換えている訳である。 こうした方法がグローバル変数を使った方法よりも優れている点は、グローバル 変数を用いるとプログラム全体でその変数の使用について注意しなければならない のに対して、ポインタを用いるとそのポインタを使っている関数と呼び出し元で のみ変数の使用を考えれば良いので、比較的に考慮する範囲が狭くなるという点で ある。これは大きなプログラムを作る場合に間違いを減らすために非常に重要な 事である。

参考

phero->level.offence を見た時に、*phero.level.offence の ような演算子の優先順位による問題が生じないかと思う人がいるかも知れません。 確かに、演算子 *. では、選択演算子 . の方が 強かったので問題が生じたのですが、 ->. では演算子の 強さは同じになっていますので、この場合には問題は生じません。何故ならば、 ->. は共にメンバーを取り出すための演算子だからです (同じタイプの演算子に強弱の違いがあっては混乱しますよね!)。

実は、このように強弱が同じ演算子が複数ある場合にはどちらから先に演算する のかという問題が生じます。このために、演算子には優先順位の他に、結合 規則というのが決められています。例えば、$5+2-3$ の場合、+- の優先順位は同じなのでこれだけではどこから計算するのか決まりません。 そこで、同じ強さの演算子の場合には結合規則によって決める事になっています。 多くの演算子の結合規則は左から右です。つまり、この計算の場合には、 $(5+2)-4$ の順に計算するようになっています。構造体の選択演算子もこれと 同じように左から右という風に決まっているために、phero->level.offence は、(phero->level).offence という順に演算されます。

右から左の結合規則を持つものは少数ですが、=,+=,! など があります。例えば、a=b=0 は、右側の = から先に演算をする事 になるので、a=(b=0) となる訳です。



Noriyo Kanayama