2通り目と3通り目の文字列読み込みでは、文字入力で大概の参考書で真っ先に使うscanf関数を紹介しましたが、個人的にはscanf関数は使いにくいなぁという印象です。
私はfgetsで改行文字ごと読み込んで改行文字をNULL文字に置き換えるほうが好きです。理屈が単純ということはそれだけバグを減らせるので。
もちろんscanfにはそれなりの良さがあるわけですが、だとしても3番目のように一度読み込んでからsscanfしますね。
練習問題1
以下のソースコードは標準入力から数値を受け取り、指定範囲内でない数値に対しては再度入力を促すプログラムである。
しかし、これは意図した動作をしない。問題点を可能な限り多くあげよ(getnum関数はget_integer_num関数のもとになった関数でエラー対策に難がありますが、そこはスルーしてください)
#include<stdio.h>
#include<stdlib.h>
int getnum(void){
char s[100];
long t;
char *endptr;
fgets(s, 100, stdin);
errno = 0;
t = strtol(s, &endptr, 10);
if (errno != 0 || *endptr != '\n' || (t < INT_MIN || INT_MAX < t))
return -1;
return t;
}
int getnum_customized(const int max, const int min){
if (max < min) return -1;
int flag0;
do{
flag0 = getnum();
if (flag0 < min || flag0 > max)
system("cls");
printf("再入力してください。\n");
} while (flag0 < min || flag0 > max + 1);
return flag0;
}
int main(void){
printf("値を入力してください。\n");
const int flag = getnum_customized(100, 0);
printf("取得した値は%dです。\n", flag);
return 0;
}
回答
-
19-21行目は19行目の時の条件を満たした場合に20,21行目を実行することを意図していると思われるが、この書き方では21行目は条件判定にかかわらず実行される。
if (flag0 < min || flag0 > max){
system("cls");
printf("再入力してください。\n");
}
が正しい。前に「エラー処理等、明確に実行文が1行しかない、と言える時を除き、原則{}はつけましょう。」と言った理由はこれ。もともと20行目はなく、このコードを書いた人曰く後から追加したらしい。
-
19行目と22行目に注目すると、条件判定がおかしいことに気がつく。つまり、先の修正を踏まえると、flag0がmaxと同値の時、再入力を求める文章が出るのにもかかわらず、実際には再入力することなdo-while文を抜けてしまう。
-
18行目に注目すると、getnum関数の戻り値チェックをしていないことに気がつく。このgetnum関数は11行目にあるようにエラー時は-1を返すので、その判定をする必要がある。つまり
if (-1 == flag0 || flag0 < min || flag0 > max){
とするべきである。
これらを踏まえ、また無駄な条件判定を減らし、getnum関数を上に上げたget_integer_num関数に置き換えると
#include<stdio.h>
#include<stdlib.h>
#include<limits.h>//in gcc
#include<errno.h>//in gcc
int get_integer_num(const int max, const int min){
//機能:標準入力を数字に変換する。
//引数:戻り値の最大値,戻り値の最小値
//戻り値:入力した数字、エラー時はINT_MIN, EOFのときはEOF
char s[100];
if (NULL == fgets(s, 100, stdin)){
if (feof(stdin)){//エラーの原因がEOFか切り分け
return EOF;
}
//改行文字が入力を受けた配列にない場合、入力ストリームにごみがある
size_t i;
for(i = 0; i < 100 && '\0' == s[i]; i++);//strlenもどき
if('\n' != s[i - 1]) while(getchar() != '\n');//入力ストリームを掃除
return INT_MIN;
}
if ('\n' == s[0]) return INT_MIN;
errno = 0;
const long t = strtol(s, NULL, 10);
if (0 != errno || t < min || max < t)
return INT_MIN;
return (int)t;
}
int getnum_customized(const int max, const int min){
if (max < min) return -1;
int buf;
while(INT_MIN == (buf = get_integer_num(max, min))){
system("cls");//Windows only. Use 'system("clear")' instead when you run on Linux or Mac.
puts("再入力してください。");
}
return buf;
}
int main(void){
puts("値を入力してください。");
const int input_num = getnum_customized(100, 0);
if(EOF == input_num){
puts("ファイル終端です");
}
else{
printf("取得した値は%dです。\n", input_num);
}
return 0;
}
練習問題2
ここでは、コンピュータがランダムに考えた数字を、回答とそれに対するヒントにより当てて行くゲームを数当てゲームと呼ぶことにします。
とりあえず乱数の範囲は5~80とし、10回まで回答できるものとします。
応答としてはこんなかんじです。
動作例
最大値80, 最小値5の間で乱数が生成されました。数当てゲームの開始です!
値を入力してください
40
もっと小さいよ
値を入力してください
20
もっと大きいよ
値を入力してください
10
もっと大きいよ
値を入力してください
15
もっと大きいよ
値を入力してください
17
もっと大きいよ
値を入力してください
19
もっと大きいよ
値を入力してください
30
もっと大きいよ
値を入力してください
35
もっと小さいよ
値を入力してください
33
もっと大きいよ
値を入力してください
34
成功!
イメージは湧いたでしょうか?というわけでこれを作ってください・・・といっても穴埋めです。以下のmain関数をいじり、必要なら関数を追加してプログラムを完成させてください。
input関数の使い方は
http://www.biboro.org/snippet/398
を参照。