はいはいscanfscanf

更新が停滞していると言われたので。
プログラミングをしていたら後輩と仲良くなれました。


あれですよ、scanfです。だいたい元々入力ストリームっていうのは文字(char)が1つずつ流れてくるものなんだから、ここから該当部分を取り出してintだのなんだののデータに直すっていうのは高レベルの話かつどこでエラーになってもおかしくない面倒な作業なわけです。
そうそう、この記事を読むには、というかscanfを使うためには正規表現の知識があった方がよいです。


よく見るのはこれですね。

scanf("%d %d", &a, &b);

でも一方で多分このようなコードも書いているわけです。

printf("aを入力してください\n");
scanf("%d", &a);
printf("bを入力してください\n");
scanf("%d", &b);

このscanfの部分だけよく見ればこう見れるはずです。

scanf("%d%d", &a, &b);

実際、同じように動作します。説明できる自信がないなら下を読んでみるといいかも?


定義:ホワイトスペース、空白文字
見えない文字全般
代表的なものは半角スペース・タブ・改行あたりでしょう。
のisspace()関数が真を返す文字と規格で定義されているかもしれません。

scanf(" ");

scanfのフォーマット文字列の「半角スペース」は「ホワイトスペース」0個以上にマッチします。「半角スペース1個」にマッチするわけではありません。「半角スペース、改行、改行」などといったものにもマッチします。「空白文字であるどれか1文字」を\sとおけば、正規表現/\s*/(//はクォーテーションですよ)を読み飛ばします。エラー処理スルーかつ分かりやすいループの書き方かつisspaceでほんとに正しいのか知らないことを前提で書けばこうでしょうかねえ。

int ch;
while(1){
  ch = getc(stdin);
  if(!isspace(ch)){
    ungetc(ch, stdin);
    break;
  }
}
scanf("%d%lf%s");

%dだのなんだのの%c以外のよくあるフォーマット指定子は最初に空白文字を読み飛ばします。イメージとしてはこんな感じでしょうか。

scanf(" %d %lf %s");

" "は前述の通り空白文字「0個以上」を読み飛ばすのでまあこれもそのまま上のと同じ動作をします。(" "で空白文字をできる限り読み飛ばし、そのあと%dなどの暗黙の?スキップ動作が0文字読み飛ばしとなる)

10 3.14 (改行)abc

入力をこのような空白文字(複数でもよい)で区切られたトークン(10, 3.14, abc)の列としてみなす場合は"%d%lf%s"のように%ナントカを連打すればいいです。

scanf("%c");

%cだけ(?)は他と比べて特殊です。これは空白文字の読み飛ばしを行いません。次の文字が空白文字でもそのまま読み取ります。getc()/getchar()と(普通に使えば)同じと考えておくといいかもしれません。
よくあるのはこれです。

printf("数値を入力してください\n");
scanf("%d", &i);
printf("文字を入力してください\n");
scanf("%c", &c);

「文字を入力してください」のあとキーボード入力待ちにならずにcには改行が代入されるパターンです。「数値を入力してください」のあと、入力は例えば

123(改行)

となり、%dによって3まで読み進められ、そのあとの状態は

(改行)

となります。そのあと%cが行われるのですが、この改行文字を読み取って完了するわけです。一番簡単な回避法は前述の半角スペースを用いることでしょう。暗黙の半角スペースがないので自分でつけてやる感じでしょうか。

printf("文字を入力してください\n");
scanf(" %c", &c);

なんだかコード上はものすごく存在感が微妙なこの半角スペースですがscanfの上ではかなり大きな役割を持っているんですよねえ。

scanf("%[0-9] %[^a-z] %[abc]", s1, s2, s3);

どこからどう見ても正規表現です。本当にありがとうございました。格納先はchar *。%sと似たものだと考えるとよい。例えば%[^a-z]は正規表現に直すと/[^a-z]+/。空白文字の読み飛ばしとかはありません。

scanf("abc%d", &a);

半角スペースでも%ナントカでもない文字はそれに正確にマッチします。この場合だと入力が"abc"で始まらないとエラーになります。


代入抑止
知っておくと幾分便利。%の後に*を書く。後ろに続くポインタに代入しません(対応する引数が不要になる、というか書いちゃだめ)。

scanf("%s%*d%s", s1, s2);


エラー処理
ループとか覚えたら正しく入力されるまで繰り返し、などをやりたくなるもの。しかしその方法は簡単ではない。

  • scanfの返り値

%ナントカによって代入に成功した数。0,1,2,3...。ただし途中でファイルの終わりに達した場合(またはファイルからの読み込み自体でエラーが発生した場合(キーボード入力ではほぼありえないだろうが))EOFを返す。
これを使ったよくあるパターン。無茶苦茶な入力をすると無限ループとなる。

while(1){
  if(scanf("%d%d", &a, &b) == 2)
    break;
  printf("Invalid input.\n");
}

例えば

abc(改行)

と入力したとする。まず%dによって空白が読み飛ばされる(0個読み飛ばされる)。次に[0-9]+(0123456789のうちどれかが1つ以上)とのマッチングに多分なるだろう。次の文字は'a'なのでエラー。scanfはここで終了し、返り値は0となる。問題はそのあともう一度scanf("%d%d")を行った場合だ。この状態で次に出て来るのは'a'である。でまたすぐ0が返る。以下ループ。
多分よくある対処法はキーボードとの対話を仮定すれば改行まで読み飛ばせば再度入力できると考えて、/(改行以外の文字)*(改行)/を読み飛ばします。

while(1){
  if(scanf("%d%d", &a, &b) == 2)
    break;
  printf("Invalid input.\n");
  scanf("%*[^\n]\n");
}

%[abc]指定と*による代入抑止と「それ以外の文字」による厳密マッチを使いました。ついでにこの方法でもEOFに達した場合、無限ループになります。ファイルからのリダイレクトの場合、再入力もくそもないのでscanfの返り値がおかしかったら即エラーでしょう。キーボードからCtrl+D(UNIX)とかされたら知りません。

結局のところ%sや%[abc]と書いただけでアウツ。何文字代入されるかわからないからです。%15sや%31[abc]などのように最大文字数を指定すれば回避できます。この数値にはヌル文字を含まないことに注意してください。それぞれchar s1[16], s2[32];が必要です。そのあとのバッファを改行までクリアするのは面倒なので省略。よく考えればできるかもしれない。バッファオーバーランは(最)重大なセキュリティホールなので絶対に配り物に含めるわけにはいきません。どこぞのer○みたいなコンソールゲームを開発しようと思っている変態さんはよく調べておく必要があるでしょう。

  • fflush(stdin)

規格では動作は未定義ですが、入力バッファをクリアする実装になっている場合があります。使う処理系のfflushがその動作をし、かつ他の処理系への移植を全く考えないならば、かなり安全で強力な方法となるでしょう。(アプリケーション開発においては場合によって可、C言語がうんたらでは不可)


文字ストリームから整数や実数値を取り出すのがいかに面倒な作業であるか少しでもおわかりいただければ幸いです。多分。あとprintfとは一応対の関数ではあるけれども全然対でないこともわかると思います。半角スペースとか違いすぎ。


※scanfのmanを見ながら書いてはいますが規格を見ながら書いているわけではありません。注意。
規格書にscanf系の規格と例が、また、wikipediaのscanfにも参考になる文章がいくらかあります。