プロが教える店舗&オフィスのセキュリティ対策術

このじゃんけんプログラムに、ゲーム終了時ユーザーとコンピュータが出した全ての手を履歴として表示したいのですがmalloc()関数を使って表示させるとしたらどういったプログラムになりますか。
*aと*b、int stage=0を最初に宣言して
void rireki(void)
{
a = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));
a[stage++] = comp;

b = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));
b[stage++] = user;
}
関数を作り、最後にprintf文で書いたのですがうまくいきませんでした。
hd[a[i]]という風に配列に配列を入れたのが駄目だったかもしれません。
for(i=0; i<draw_no+lose_no+win_no; i++){
printf("%d回目 ユーザ:%c コンピュータ:%c\n", i+1, hd[b[i]], hd[a[i]]);
}
また本の課題なのですが、malloc関数は後に出てくるのでmalloc関数を使わずに履歴表示できるかもしれないのですが
その場合配列をあらかじめ宣言して格納する手段になるのでしょうか。
よろしくお願いします。

/*
課題3-6
*/

#include <time.h>
#include <stdio.h>
#include <stdlib.h>

int user;/* プレーヤの手 */
int comp;/* コンピュータの手 */
int win_no;/* 勝った回数 */
int lose_no;/* 負けた回数 */
int draw_no;/* 引き分けた回数 */

char *hd[] = {"グー", "チョキ", "パー"};/* 手 */

/* initialize関数の宣言 */
void initialize(void);

/* jyanken関数の宣言 */
void jyanken(void);

/* count_no関数の宣言 */
void count_no(int result);

/* disp_result関数の宣言 */
void disp_result(int result);

/* confirm_retry関数の宣言 */
int confirm_retry(void);

/* メイン関数 */
int main(void)
{
int judge;/* 勝敗 */
int retry;/* もう一度 */

initialize();/* 初期処理 */

do{
jyanken();/* じゃんけん実行 */

/* コンピュータとプレーヤの手を表示 */
printf("私は%sで、あなたは%sです。\n", hd[comp], hd[user]);

judge = (user - comp + 3) % 3;/* 勝敗を判定 */

count_no(judge);/* 勝/負/引分け回数を更新 */

disp_result(judge);/* 判定結果を表示 */

retry = confirm_retry();

}while(retry == 1);

printf("%d勝%d敗%d分けでした。\n", win_no, lose_no, draw_no);

return (0);
}

/*--- 初期処理 ---*/
/* initialize関数の定義 */
void initialize(void)
{
win_no = 0;/* 勝った回数 */
lose_no = 0;/* 負けた回数 */
draw_no = 0;/* 引き分けた回数 */

srand(time(NULL));/* 乱数の種を初期化 */

printf("じゃんけんゲーム開始!!\n");
}

/*--- じゃんけん実行(手の読み込み/生成) ---*/
/* jyanken関数の定義 */
void jyanken(void)
{
int i;

comp = rand() % 3;/* コンピュータの手 (0~2) を乱数で生成 */

printf("\n\aじゃんけんポン …");
for(i=0; i<3; i++)
printf(" (%d)%s", i, hd[i]);
printf(":");
scanf("%d", &user);/* プレーヤの手を読み込む */
}

/*--- 勝/負/引き分回数を更新 ---*/
/* count_no関数の定義 */
void count_no(int result)
{
switch(result){
case 0: draw_no++;break;
case 1: lose_no++;break;
case 2: win_no++;break;
}
}

/*--- 判定結果を表示 ---*/
/* disp_result関数の定義 */
void disp_result(int result)
{
switch(result){
case 0: puts("引き分けです。");break;/* 引き分け */
case 1: puts("あなたの負けです。");break;/* 負け */
case 2: puts("あなたの勝ちです。");break;/* 勝ち */
}
}

/*--- 再挑戦するか確認 ---*/
/* confirm_result関数の定義 */
int confirm_retry(void)
{
int x;

printf("もう一度しますか … (0)いいえ (1)はい:");
scanf("%d", &x);

return (x);
}

A 回答 (5件)

>void rireki(void)


>{
>a = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));
>a[stage++] = comp;
>
>b = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));
>b[stage++] = user;
>}

コールされるのはどのタイミングでしょう?
そして、2回目以降にコールされた時、前回確保したaとbはどうなるでしょう。
# さらにその時のcompとuserには何が入っているでしょう?
そして、rireki()がコールされるとstageがコール前より「2つ」増えています。
# 上記の場合だと、b[]への代入はバッファオーバーランしている可能性が高いです。

>hd[a[i]]という風に配列に配列を入れたのが駄目だったかもしれません。

1回目はa[0]には正しい履歴が入ります。
2回目にrireki()をコールした時に前回確保したaとbがメモリリーク(free()で開放する為のポインタが上書きにより失われる)し、a[]への登録がバッファオーバーラン(a[2]に書き込みしようとします。stageが2ですから)、a[0]とa[1]は「不定値」が残っています。
# malloc()は確保したメモリ内容に関してなにもしません。(calloc()なら0クリアしてくれますが…)
繰り返していって……
hd[a[0]]を参照した時に不定値になっていますので、高確率でhd[]の範囲外をアクセスしprintf()内で吹っ飛びます。


単純な方法だと…
malloc()ではなくrealloc()で領域を「拡張」する。
以前記録した履歴(メモリ内容)は拡張後も保証されます。
stageの加算は履歴に「追加後」に行う必要があります。
ただし、realloc()は失敗する可能性もあるのでその辺りの対処が必要。
# a = (int *)realloc(a, sizeof(int) * (draw_no+lose_no+win_no));
# という使い方は正しくない。
メモリのフラグメントが発生しやすくなる。
realloc()を繰り返すのはコストが高い。

手間を掛けるのであれば、リスト構造体にして追加していく。
というのもありますな。
# 片方向リストで十分でしょう。

この回答への補足

回答ありがとうございます。
言われてみると酷い間違えばっかでした。

do~while文の前に
a = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));
b = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));

rireki関数の呼び出しはjyanken関数の後に宣言してみました。

/* rireki関数 */
void rireki(void)
{
a[stage] = comp;
b[stage] = user;
stage++;
}

この場合履歴の表示まで実行できたのですが、グーなどの文字ではなく違う結果になってしまいました。

補足日時:2011/08/23 10:35
    • good
    • 0
この回答へのお礼

mallocではなくreallocというものを使ってやってみれば良いんですね。
勘違いしていました。ありがとうございます。

お礼日時:2011/08/23 11:14

まずは「こうすればもっと (私の主観に基づいて) いいプログラムになる」という指摘:



・「双方の出した手」を構造体で覚えておくと楽.
・グー/チョキ/パーを表す数値や勝敗を表す数値はいわゆる「マジックナンバー」なので enum なり #define なりする.
・勝敗を決めるときに
judge = (user - comp + 3) % 3
のような謎の式を直接書かない. 配列を参照するか, せめて関数にする.
・勝敗のカウンタは配列にして count_no を捨てる. メッセージも配列にすれば disp_result も消せる.
・再挑戦するかどうかを確認する関数がコメントでは confirm_result なのに実際の定義が confirm_retry なのはおかしい. そもそも「~関数の定義」などというコメントは百害あって一利なし.
・過去の手を覚えておく変数にはそれなりな名前を与える. 「ふつ~の人」が a とか b とかいう変数名を見て「これが過去の手を覚えているんだ」と思えるでしょうか?

で realloc を使うとすれば, 「1回やるたびに realloc」はさすがにコストが馬鹿にならないので #2 で触れられているように「大きめの領域を確保しておき, 足りなくなったら realloc」の方が速度は稼げる. 連結リストにするかどうかは.... まあ今回の場合は好みだな. ランダムアクセスしたいなら連結リストは外すだろうけど, シーケンシャルアクセスしかしないっぽいので連結リストもないわけじゃない.

あと恒例の突込み (苦笑) ですがメモリ確保系の関数で「0バイトを確保」したときに何が返るかは処理系依存>#3. NULL が返っても文句は言えないです.
    • good
    • 0
この回答へのお礼

アドバイスありがとうございます。
細かいところまで目が利くようになれるように頑張りたいです。
judge = (user - comp + 3) % 3は今やっている参考書からの引用です。
resultとretryは単純にタイプミスです。すいません。
~関数の定義は、入門書の方で毎回書かれていたので一応書いてました。
aとかの変数名は適当になってました。

お礼日時:2011/08/23 13:42

>draw_no+lose_no+win_noの値に気付き、do~while文の後に


>a = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));
>b = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));

ループ抜けた後では無意味です。
# それまでの履歴がないでしょう?

ループ中でmalloc()すれば、最初と同じくメモリリークする上に以前のデータが引き継がれません。

別にmalloc()して、以前のデータを新しい領域にコピーして、以前の領域をfree()して……とかやるくらいならrealloc()を使えば済むハナシです。
実装によって異なりますが…realloc()で拡張する場合にやっているのは……
・元のメモリブロックの後ろに要求された空きがあればメモリブロックを拡張して終わり。
・空きがない場合は拡張後のサイズでメモリ確保し直して、元の領域のデータをコピーし、以前の領域はfree()。
・拡張後のサイズでメモリ確保できない場合はなにもしないでNULLを返却し、失敗を通知。
って感じの処理を行っています。
# 縮小する場合は、たいてい成功するのでメモリブロック縮めて終わり。(溢れたデータについては保証しない)


よって…勝負回数が不明である以上、malloc()1回で履歴を残す。というのは無理です。
# 大きい領域を確保すれば…にしてもそれを越えればアウトですし。(何度も書かれてますけど)
    • good
    • 0
この回答へのお礼

ありがとうございます。
realloc()でやってみようと思います。
reallocの動作の説明までしてもらいありがとうございます。

お礼日時:2011/08/23 11:47

>do~while文の前に


>a = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));
>b = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));

メインループの前に(draw_no+lose_no+win_no)はいくつになりますか?

>rireki関数の呼び出しはjyanken関数の後に宣言してみました。

「宣言してみました」は正しくないのですが…
rireki()をコールすると、a[]とb[]への書き込みでヒープを破壊していきます。
勝負の回数が増えたところで、メインループの前にmalloc()で確保した領域が(サイズにsizeof(int) * (draw_no+lose_no+win_no)を指定しているから)自動的に増えて行くなんてコトはありません。
malloc()した時点での各変数の値を参照するだけですから、
その後でdraw_noやlose_no、win_noを増やしてもmalloc()で確保した領域のサイズは増えません。

メインループの前では(draw_no+lose_no+win_no)は0でしょうから、「サイズ0のメモリブロック」を確保しただけになります。
そこにデータを追加していけば待っているのは領域破壊(バッファオーバーラン)だけです。
固定的にサイズを取得しても、それを越えればやはりバッファオーバーランです。

>この場合履歴の表示まで実行できたのですが、グーなどの文字ではなく違う結果になってしまいました。

バッファオーバーランで破壊していますから、原理的には何が起こっても不思議ではない…です。


あと…この程度のプログラムならmalloc()等での動的確保を開放せずにプログラム終了しても問題ないでしょうが、free()でちゃんと開放するように習慣づけた方がいいでしょう。

この回答への補足

なんども入門書を読み直してるうちに自動的に増えるということに、勘違いしていたことに気付きました。
draw_no+lose_no+win_noの値に気付き、do~while文の後に
a = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));
b = (int *)malloc(sizeof(int) * (draw_no+lose_no+win_no));
としてみましたが、rireki関数には格納できないのでたぶんどこで宣言してもmalloc関数ではできないのでしょうか。
free()関数は終わりに書いてあります。
free(a);
free(b);
と記述しています。

補足日時:2011/08/23 11:20
    • good
    • 0

こんにちは。

業界でメインプログラマしている者です。

ソース拝見しました。rireki()を具体的にどこで呼んでいるのかが不明確ですが、恐らく1勝負毎ですね。
(do whileの最後辺り)
1回戦う毎に結果をメモリに残しておいて最後にprintfと言う事なんでしょうが、これは設計が分かれる所です。

大別すると、
1.初めに固定長バッファ(主さんの言う所の配列)を用意して決まった回数勝負する。
2.じゃんけんの回数を決めたくないから勝負する度にメモリ領域を確保してそこにしまっておく。

主さんの理想は2かと思います。

rireki()の呼び方が上述の通りと仮定すると、まずrireki()内でmallocしているのが問題です。
1勝負毎に別の空きメモリ空間に、その時の勝負回数分のサイズを別途用意してしまいます。
(1勝負する度にどんどんメモリリークする)

この場合、2を実現するだけならmallocではなくreallocを使うのが現実的ですが、私はあまりお勧めしません。
(物によっては処理負荷が大きく、メモリの断片化も懸念されるので、業務の性質上、部下がやっていたらやめさせます)

ただこの位の規模のじゃんけんゲームなら負荷も気にする必要は無いでしょうし、練習と言う意味では有りです。


reallocを使っての2の設計をもう少し細かくした別のアプローチとして、
・1戦する度にreallocでは無く、予めmallocでN回分の領域を取っておいて、N回を超えたらもうN回分realloc
と言う形でリアロケートの頻度を少なくする事で処理負荷と断片化を抑制する方法が考えられます。

実装効率、動的性と負荷を考えるとバランスはこう取るのが妥協ラインかと思います。後はNをどの位にするかでしょう。


もう少し進んでC++になるとVectorやlistにプッシュしていく。と言う技術をその内目にすると思います。
C++標準のSTLで実現する場合、内部ロジックはコンパイラによりますが、大体N回おきにreallocです。

reallocが嫌なら別途mallocした領域(Nより大きい)に今の内容をコピーして次の受け皿とする。
その後今まで使っていた領域をfreeで解放する。

とも出来ますが、結局断片化等の理由でアロケートサイズが足りなければmallocは失敗します。


私がもし、業務でやるならこうしますと言う案で言うと、

メモリアロケータやlistから自作出来るのであれば、list内部にアロケート機構を一切絡ませずに動的生成部分は外に出す。
その後、じゃんけんの度に勝負1回分をmallocしてlistに繋いでいく。と言う方法が取れます。

このじゃんけんの結果を格納するメモリをページ分けしておけば、その部分の断片化は起こらなく出来ます。
その為メモリをかなり有効に活用でき、延々と履歴を取れる様なイメージには近づけられると思います。

ただメモリは有限なので、どんなに頑張ってもNの回数を無限には出来ません。
無限にしたいならキュー構造等を使って規定回数を超えたら古い物から削除して新しい物を上書き。

とする事で妥協する位です。


少し話が難しくなったかも知れませんが、まずはreallocを使って実装してみるのが良いのでは無いでしょうか。
それが出来る事で、reallocの問題点を理解・認識して次に進めれば良いと思います。
    • good
    • 0
この回答へのお礼

reallocについては入門書には全く触れられていなかったので今の段階ではよくわかりませんが、今やっている本に載っているみたいなのでこれからみてみようと思います。
ありがとうございます。

お礼日時:2011/08/23 11:12

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!