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

標準入力 scanfとgetsの小技 〜scanfの魔界からの脱出

scanfの書式文字列の書き方にはなかなか厄介な点があり、
注意点として、標準入力scanfの使用上の注意点
@ データ読み込み時の区切り記号への注意点
A 入力バッファに改行ッコードが残ってしまう問題
B 空白を含んだ文字列の入力の問題
があることを書きました。

このページではそれらの注意点、問題点の回避策について書いておきます。
内容は以下の通りです。

Cは空白やタブを区切りとして入力データを読み取りますが、カンマ( , )を区切り記号にして
読み取ることもできますので、カンマ区切りでのデータ読み取りについては、
「1. カンマ( , )を区切りにして数値の入力」、
「2. カンマ区切りで文字列を読み込む」
入力バッファに改行コードが残る問題については「3.入力バッファに残る改行コードの回避策」
空白を含んだ文字列の入力については「4.空白を含んだ文字列の入力」
またCでは配列の範囲を超えて文字列を取り込めてしまうという問題があります。この点への
対応策については「5.文字列の長さのチェック」


以下、このページのプログラムはBorland C++55 でコンパイルしています。


1. カンマ( , )を区切りにして数値の入力


書式文字列に、%d,%d,...というように%dをカンマ( , )で区切って指定するとカンマで区切られた数値を
scanfで入力できる。

scanf4.c

#include <stdio.h>

void main(void)
{
int x,y,z;
scanf("%d, %d, %d",&x,&y,&z);
printf("/%d/\n",x);
printf("/%d/\n",y);
printf("/%d/\n",z);
}


実行例

@ちゃんと、カンマを区切りとして数値をとりこんでいる。

c:\bcc55\Bin>scanf4
356,38,76
/356/
/38/
/76/


A数字の前の空白は読み飛ばされて、数値は正しくとりこまれている

c:\bcc55\Bin>scanf4
55, 67, 12 /55/ /67/ /12/


B数値の後ろに空白があると正しく取り込めない

c:\bcc55\Bin>scanf4
4, 67 , 12
/4/
/67/
/1/


C空白区切りでは正しく取り込めない

c:\bcc55\Bin>scanf4
4 26 10
/4/
/256/
/1/


D 文字は正しく取り込めない

c:\bcc55\Bin>scanf4
5,30,a
/5/
/30/
/1/


AとBを比較するとわかりますが、読み込むべきデータが現れるまでその前にある空白は読み飛ばして
くれています。データの後ろに空白がくると正しく処理できなくなります。


「scanf4.c」の書式文字列中には空白を含み、"%d, %d, %d" となっていますが、この空白はなくてもscanfの
動作には影響がないようです。以下は書式文字列に空白がないケース。

<scanf4a.c>

#include <stdio.h>

void main(void)
{
int x,y,z;
scanf("%d,%d,%d",&x,&y,&z);
printf("/%d/\n",x);
printf("/%d/\n",y);
printf("/%d/\n",z);
}

実行例
数値の前に空白のあるパターンですが正しく処理しています。
c:\bcc55\Bin>scanf4a
  4,    11,   3
/4/
/11/
/3/

区切る記号を/(スラッシュ)として指定することもできます。
/で区切って年号を入力する例

<scanf7.c>

#include <stdio.h>

void main(void)
{
int y,m,d;
scanf("%d/%d/%d",&y,&m,&d);
printf("year:%d\n",y);
printf("month:%d\n",m);
printf("day:%d\n",d);
}


実行例

c:\bcc55\Bin>scanf7
2022/12/15
year:2022
month:12
day:15

実際のところ、このscanfの動作はどういうルールに基づいているのかよいうと
scanfの以下の仕様に基づくものです。
scanfの第一引数に指定する書式文字列の中で、変換支持子以外の文字が記述されている場合
それらの文字はそのまま入力されることが要求されます。その通りに入力されなかった場合は
scanfは正常に処理を行いません。

以下のscanf7a.c では、書式文字列の最初に、input: という記述がりますが、
この場合、input: という文字列を入力しないとscanfは正常にデータを読み込みません。


#include <stdio.h>

void main(void)
{
int y,m,d;
scanf("input:%d/%d/%d",&y,&m,&d);
printf("year:%d\n",y);
printf("month:%d\n",m);
printf("day:%d\n",d);
}

実行例

c:\bcc55\Bin>scanf7a
input:2023/1/1
year:2023
month:1
day:1

以下の2つの実行例ではscanfは正常に処理ができていません。

c:\bcc55\Bin>scanf7a
input2023/1/1
year:1
month:256
day:1

c:\bcc55\Bin>scanf7a
input::2023/1/1
year:1
month:256
day:1


2. カンマ区切りで文字列を読み込む

これはスキャン集合指定子というものを使います。
スキャン集合指定子は[ ]で指定した文字だけを読み込む指定子で
%[xy]
と書くと、xかyしか読み取りません。指定子で指定された文字以外の文字が現れると読み取りを
中断します。
例えば、%[0123456789] とすると0〜9の数値の文字しか受け取りません。

また、^を集合の前に指定すると、読み取らない文字の集合を指定することになります。
この機能を使うことにより、カンマ区切りで文字列を読み込むことが実現できます。



<scanf6.c>


#include <stdio.h>

void main(void)
{
char x[10],y[10],z[10];
scanf("%[^,], %[^,], %[^,]",x,y,z);
printf("/%s/\n",x);
printf("/%s/\n",y);
printf("/%s/\n",z);
}


c:\bcc55\Bin>scanf6
we,are,the
/we/
/are/
/the/


空白が含まれていても区切りとは見なさずに、カンマ区切りで読み取られている
ただし文字列の前の空白はスキップされ取り込まれない。


c:\bcc55\Bin>scanf6 we , are , the /we / /are / /the /

以下のscanf6-2.cは、最初のデータは小文字のアルファベットのみ、2つ目は数字の文字のみ、
3つ目はどんな文字でもOkとしてカンマ区切りで読み取る例です。

<scanf6-2.c>
#include <stdio.h>

void main(void)
{
char x[10],y[10],z[10];
scanf("%[a-z], %[0-9], %[^,]",x,y,z);
printf("/%s/\n",x);
printf("/%s/\n",y);
printf("/%s/\n",z);
}


実行例
c:\bcc55\Bin>scanf6-2
abc,123,123xyz,
/abc/
/123/
/123xyz/


最初のデータとしてabc123を入力すると、abcまで読み込んだところで正常な処理が中断します。

c:\bcc55\Bin>scanf6-2
abc123,123abc,123xyz,
/abc/
/槻@/
/ ミ@/



入力されるデータ数が決まっているときはこの方法で読み込むことができるが、
読み込むデータ数が不定の時はこのプログラムでは対応できない。



3.入力バッファに残る改行コードの回避策
scanfの使用上の注意点に書いた通り
文字列 → 1文字 →文字列 という読み取りをしようとすると、入力バッファに残る改行コードの
せいでおかしな動作になります。

入力バッファに残る改行コードの回避策について書きます。
1文字読み取りのscanfの書式の指定で、%*c%c という指定をしてやると改行コードをうまく処理して
1文字を読み込んでくれます。
*は代入抑制文字で、読み取った値を変数に代入させない働きがあります。=>改行コードを捨てるという
処理になります。

#include <iostream.h>

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

実行例

c:\bcc55\Bin>scanf2m
Hello
/Hello/
1
/1/
World
/World/



4.空白を含んだ文字列の入力
空白を含んだ文字列の入力にはgetsを使います。

gets.c

#include <stdio.h>

void main(void)
{
char s[10];
gets(s);
printf("/%s/\n",s);
}


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

c:\bcc55\Bin>gets
Hello boy
/Hello boy/


c:\bcc55\Bin>gets
Hello my World! Oh boy!
/Hello my World! Oh boy!/

この実行例では、文字列が配列の範囲を超えているのに、文字列を取り込んでしまっていますが、
この後、このprogramはここで動作を停止終了してしまいます。
これはCの文字列の取り扱いで問題が発生する典型パターンの一つです。
この問題に対応するには、以下のように入力された文字列の長さをチェックするというのが
ひとつの対応策になります。


5.文字列の長さのチェック
文字列の配列の長さを超える文字列をチェックし入力をやり直しさせる


#include <iostream.h>

void main(void)
{
char s[5];
inp:
scanf("%s",s);
if (strlen(s)>4) {printf("too long!\n"); goto inp;}
else printf("%s",s);

}


c:\bcc55\Bin>scanf5
Hello
too long!
World
too long!
boys
boys
c:\bcc55\Bin>


6. getsで1行読み込んだ後に区切り記号で分割

「2. カンマ区切りで文字列を読み込む」で書いた方法では、読み込むデータ数があらかじめ決まっている
場合でないと、対応できませんでした。

読み込むデータ数が決まっていない時は以下の方法で可能です。

Cには読み込んだ1行を区切り記号で分割してトークン化するための関数がないので、
strtok関数で1個づつ読み込んでいきます。

詳細な説明は「標準入力の小技〜分割して入力」にあります。

空白区切りで文字列を分割

<split0.c>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char s[30],*token;
gets(s);
token=strtok(s," ");
while(token != NULL) {
printf("%s\n",token);
token=strtok(NULL," ");
}
return 0;
}


カンマ区切りで数字を読み込み、整数に変換してそれらの数値の総和を求める。

<split2.c>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int sum;
char s[30],*token;
gets(s);
sum=0;
token=strtok(s,",");
while(token != NULL) {
sum=sum+atoi(token);
token=strtok(NULL,",");
}
printf("%d\n",sum);
return 0;
}










TopPage