パソコン活用研究シリコンバレー(C、C++、の活用研究)

標準入力 scanfの使用上注意点
〜バグの魔界scanf

------------------------------------summery-----------------------------------------------------
scanf in C program is a hotbed of bugs. Reading strings into arrays, you should be careful.
When you use "scanf", please note that whitespace is recognized as a delimiter and
newline characters remain in the buffer. 
とにもかくにも、C言語の初心者にはscanfは厄介なバグの温床になります。scanfを使うときの
注意点をいくつかまとめてみました。
-------------------------------------------------------------------------------------------------

バグの魔界scanfの森へようこそ。scanfはバグの温床。
バグの魔界という副題をつけたほどに、このscanfというやつにはわけのわからないバグ(仕様?)があり
ます。うかつに使うとバグの魔界に取り込まれてしまいます。

scanfの基本的な使い方は、画面に出力(printf)・キーボードから入力(scanf)・変数
 に書きましたので
そちらをご参照下さい。
ちなみに、標準入力とはPCの場合は一般的にはキーボードになります。つまりキーボードからの入力ということ。

基本的な使い方は知ってるさという方々、では、魔界へさっそく旅立つとしましょう。

※(ご参考)標準入力からの値の入力に関するショートプログラムは以下のページにあります。
値の入力
標準入力の小技〜分割して入力

1 文字列と配列の注意点 + scanfは空白を区切りとして認識する


最初はこんなプログラムでテストしてみます。
5byteの配列(添え字が5の配列)を用意して、3回scanfで読み込んで、
3回とも入力データを / / で囲ってそのまま出力という超シンプルプログラム。
ちなみに今回はBorland C++ 55でC++としてコンパイルしたので、includeファイルはiostream.hのままになってます。
通常のCのコンパイラなら stdio.h になるところです。あしからず。

scanf1.cpp
#include <iostream.h>
void main(void) {
char s1[5],s2[5],s3[5];
scanf("%s",s1);
printf("/%s/\n",s1);
scanf("%s",s2);
printf("/%s/\n",s2);
scanf("%s",s3);
printf("/%s/\n",s3); }

まずは
Hello
my
World
と3回に分けて入力してみます。

c:\bcc55>scanf1
Hello
/Hello/
my
/my/
World
/World/

これは期待通りの動き(出力)ですね。上出来です。

でも、実は手放しでは喜べません。一抹の不安が胸をよぎります。
何が不安かって?
だって、char s1[5]と言う配列の宣言は、s1[0],s1[1],s1[2],s1[3],s1[4]が用意されるわけですよね。
で、C言語の文字列は最後にヌル文字(\0)をつける約束ですよね。ヌル文字が1文字文の場所を
とるので、s1[5]の宣言では、実際の文字は4文字までしか格納できないはず・・
なのに、Hello と5文字入ってる。Worldもそうだ。
なんか怪しいくないですか。いやな予感がしますよね。
えっ、そうなの?という人。まだCの超初心者。
怪しいぜ、という人はCの初心者
Cはそういうもんさ、と言う人は中級以上ですね。

次は、
Hello my World
と1行で入力してみます。

c:\bcc55>scanf1
Hello my World
/Hello/
/my/
/World/

/Hello/
/my/
/World/
と1行が空白を区切りにして3つに分かれました。
scanfはデフォルトでは、空白やTab、改行を区切りとして入力するので、このような動作になります。
英語の場合(印欧語の場合は押しなべてそうですが)、空白を区切りとして認識するのは合理的な仕様と
言えますよね。
まあ、一応ここも仕様通りの正常な動きをしているように見えます。まだ魔界の入口なのでバグには遭遇
していないようです。・・ホントかなあ


じゃ、次は
HelloMyWorld
て入力してみよう。
一気に12文字だぞ。

c:\bcc55>scanf1
HelloMyWorld
/HelloMyWorld/
good
/good/
boy
/boy/
1
/1/
2
/2/
3
/3/

れれっ、動作がおかしい。
そのあと、good, boy, 1, 2, 3 と計6回も入力できてしまった。
いよいよ魔物が現れやがったな。って感じですか。

C言語で配列で文字列を扱う場合は要注意です。
C言語では配列の添え字チェックはされません。BASICでは配列の添え字を超えて入力しようとすると
"out of range"などというエラーを出してくれますが、Cでは平気で添え字を超えて格納できるし、読み出し
もできてしまいます。
また、文字列の終わりは\0(ヌル文字)と決まっているので、配列の添え字を超えても、\0が現れるまでを
文字列として認識します。もう、ほとんどアセンブラに近いプリミティブさですね。

こんな風に、C言語は配列で確保した領域を超えて文字列を格納できてしまうので、その場合
こんな風に動作がおかしくなることもあります。場合によっては他のデータの領域に食い込んで
データを壊す可能性もあるし、C言語の配列は取扱いに気を使います。

文字列の長さに制限を掛けている場合は、strlenなどで入力した文字列の長さをチェックするなど、
自分で入力チェック部分を実装しておく必要があります。
scanfとgetsの小技にstrlenによる文字列チェックについて書きましたので、参照して下さい。


2 入力バッファに改行コードが残る


次のテストプログラムは、2番目のs2を、文字列 −>1文字 に変更したものです。
意図としては、文字列入力 −>1文字入力 −>文字列入力
と言う風な動作を期待しています。

scanf2.cpp
#include <iostream.h>

void main(void)
{
char s1[5],s2,s3[5];
scanf("%s",s1);
printf("/%s/\n",s1);
scanf("%c",&s2);
printf("/%c/\n",s2);
scanf("%s",s3);
printf("/%s/\n",s3);
}

では、まず
Hello
と入力

c:\bcc55>scanf2
Hello
/Hello/
/
/
my
/my/

そうすると
/Hello/
/
/
となった。

実際は入力バッファには
Hello<\n>(=改行コード)
というデータが取り込まれており、scanfが入力バッファからデータを取りこむときは、文字列の
データだけを取り込み、改行コードはバッファに残したままになるようです。
で、2つめのscanfは1文字としてバッファから改行コードを取り込んでしまったようです。
魔界な仕様ですね。

次は
Hello my World
と入力してみます。

c:\bcc55>scanf2
Hello my World
/Hello/
/ /
/my/

s2はHelloの後の空白文字を取り込んでいますね。
どうも、文字列 −> 1文字
という動作はできないようですね。それがC言語のscanfの仕様のようなのでしようがないですね。

s2に意図通りに1文字入力する方法がないかというと、ちょっとした工夫でそれも可能です。
それはscanfとgetsの小技に書きましたのでご参照下さい。

例によって、5文字をはるかに超える文字列を入力してみます。

c:\bcc55>scanf2
HelloMyWorld good boy 1 2 3
/HelloMyWorld/
/ /
/good/
/boy/
/ /
/1/

やっぱり、動作は正常ではないですね。


3 空白の入った1行の読み込み


今までは1行で1つの文字列の入力を意図していたプログラムでしたが、
次のは、1行で空白を区切りとして3つの文字列の入力を意図したプログラムです。
scanf("%s %s %s",s1,s2,s3);
とすることにより、1行からいっきに3つの文字列を取り込みます。文字列の区切りは空白です。
(scanfで文字列の入力をする場合、区切りは空白かTab、改行コードがデフォルトとなっています)

#include <iostream.h>

void main(void)
{
char s1[5],s2[5],s3[5];
scanf("%s %s %s",s1,s2,s3);
printf("/%s/\n",s1);
printf("/%s/\n",s2);
printf("/%s/\n",s3);
}

まずは
Hello My World
と入力してみます.

c:\bcc55\Bin>scanf3
Hello My World
/Hello/
/My/
/World/

これは意図通りに動作しています。

次に試しに、
Hello<改行>
my<改行>
World<改行>
と入力してみると、3個の文字列を入力し終わってから出力しました。
これも仕様通りの動きですね。

c:\bcc55\Bin>scanf3
Hello
my
World
/Hello/
/my/
/World/


では例によって、意地悪してみます。
4文字をはるかに超える文字列を入力してみます。

c:\bcc55\Bin>scanf3
HelloMyWorld Hello My World
/HelloMyWorld/
/Hello/
/My/
World
boy
/World/
/World/
/boy/


やはり正常には動作しませんね。


4 まだまだある


さて、ここでひとつ困ったことがあります。
scanfは空白を区切りとして認識するので、空白を含んだ文字列の入力ができません。
scanfでも何か裏技があるだろう? いやどう工夫しても、逆立ちしてもscanfではできないので
そういう場合はgetsを使います。

また、空白ではなく、カンマ( , )を区切りにしたい場合がありますよね。
文字列ではなく、数値の入力の場合はカンマ( , )を区切りにして
1,10,100
のように入力させることも可能です。

これらの解決方法についてはscanfとgetsの小技に書きましたのでご参照下さい。

(参考)
BASICのINPUTで文字列を読み込む場合、文字列の間にある空白はそのまま読み込まれます。
キーボードからの入力 その2 参照
N-88Basic(エミュレータ上)  





TopPage