gets()やscanf()を使わずに標準入力する
本記事は、C言語で簡単なプログラムを書ける程度の知識を持っていることを前提として書いてあります。内容に誤りがあったり、仕様から外れた未定義動作があるかもしれません。C89を想定していますがC99やgccの拡張機能で書いてある部分があるかもしれません。
C言語で標準入力する場合、fgets()
を使って文字列として受け取り、それを必要に応じて加工します。市販のテキストでは、文字列の取得にgets()
、数値の取得にscanf()
が用いられていますが、これらにはバッファオーバーフローや想定外の入力といった危険性があるので基本的に使われません。ただし、fgets()
を使えば大丈夫、というものでもありません。
fgets()とは
fgets()
は、行を単位として文字列を取得する関数です。最後まで読み込むとNULLを返します。また、キーボード入力の改行コードも取得されます。stdin.h
が必要です。
#include <stdio.h>
char *fgets(char *buf, int n, FILE *fp);
fgets()
は、3つの引数が必要です。1つ目は読み込んだ文字列を格納する配列のアドレス、2つ目は格納できる文字列の長さ、3つ目はストリーム(stdin
)です。細かい仕様は専門書にまかせるとして、実際に使ってましょう。
#include <stdio.h>
enum {
MAX = 32
};
int main(void)
{
char buf[MAX];
printf("Type: ");
fgets(buf,MAX,stdin);
puts(buf);
return 0;
}
$ ./a.out
Type: hello, world
hello, world
$
第1引数にchar型の配列bufの先頭アドレスを、第2引数に定数で定義したMAX(=32バイト)を、そして第3引数にstdinが指定されています。この中で注意が必要なのは、第2引数です。
第2引数のバッファサイズは、受け取る文字列だけではなく、終端文字であるnull(\0
)も考慮します。つまり受け取る文字列+1バイト分のバッファサイズが必要になります。上記の例だと、キーボードからhello, world
を受け取るので、文字数と決定のリターン(\n
)で13バイト、そしてnull(\0
)で1バイトと、合計14バイトのバッファサイズが必要になります。
#include <stdio.h>
#include <string.h>
enum {
MAX = 10
};
int main(void)
{
char buf[MAX];
int i;
printf("Type: ");
fgets(buf,MAX,stdin);
printf("Array size : %d\n", sizeof buf);
printf("String length: %d\n", strlen(buf));
for (i = 0; i < MAX; i++) {
printf("%2d: %c\n", i+1, buf[i]);
}
return 0;
}
バッファサイズを超えて受け取った場合、サイズ-1以降の文字列は削られ、最後の要素がnullターミネートされます。
$ ./a.out
Type: hello, world
Array size : 10
String length: 9
1: h
2: e
3: l
4: l
5: o
6: ,
7:
8: w
9: o
10:
$
上掲の出力をみると、配列の要素数が10に対して文字長が9になっています。これは、10番目の文字に改行コードではなくnull文字が挿入されているためです。これを防ぐには、十分なサイズを確保しておくか、改行コードが存在するかを確認する関数を用意します。多分、前者だけでいいです。
static int isLineFeed(const char *s)
{
if (s[strlen(s)-1] == '\n') { // strchr()でもよい
return 1;
}
return 0;
}
以上がfgets()
の基本的な動作です。前述したよう、文字列に入力時の改行コードが含まれています。puts()
で文字列を出力する場合、この改行コードが邪魔になるので、次の項で削り方を紹介します。
面倒なら、printf()
やfputs()
でも問題ありません。どれを選んでも速度は変わらないと思います(「出力関数による速度の差について」)。
#include <stdio.h>
#include <string.h>
enum {
MAX = 32
}
int main(void)
{
char buf[MAX];
printf("strtok: ");
fgets(buf,MAX,stdin);
printf("%s", buf);
printf("strlen: ");
fgets(buf,MAX,stdin);
fputs(buf, stdout);
return 0;
}
文字列の改行コードを削る
puts()
で出力する場合は、改行が二重で表示されますので、文字列の改行コードを削ります。その方法は、筆者が知る限り2通りあります。一つはstrtok()
を使ってトークンに分解する方法、他方は改行コードのアドレスにnullを代入する方法です(null文字から一つ前のアドレス)。いずれもstring.h
が必要です。なお、数値にする場合はこの作業はしなくても大丈夫だと思います(本当に無視してよいのか分かりません)。
#include <stdio.h>
#include <string.h>
enum {
MAX = 32
};
int main(void)
{
char buf[MAX];
// トークンに分解する方法
printf("strtok: ");
fgets(buf,MAX,stdin);
strtok(buf,"\n"); // 書くのが簡単
puts(buf);
// nullを代入する方法
printf("strlen: ");
fgets(buf,MAX,stdin);
buf[strlen(buf)-1] = '\0'; // '\n'に'\0'を代入
puts(buf);
return 0;
}
$ ./a.out
strtok: hello,
hello
strlen: world
world
$
文字列から改行コードが消えて、puts()
による改行のみが確認できます。なお、連続してfgets()
を使う場合、その都度バッファ(ストリーム)を解放する必要があります。これについては、「forループの中で使う」で詳述します。
数値に変換する
fgets()
で取得したのは文字列ですから、このままでは計算ができません。そこで、文字列を数値に変換します。方法はいくつかありますが、よく使うものとして、atoi()
とatof()
があげられます。それぞれ、Ascii to Integer、Ascii to Floatの略です。なお、atof()
の戻りの型は、double型で指数表記にも対応しています。これらの関数を使うには、stdlib.h
が必要です。
#include <stdio.h>
#include <stdlib.h>
enum {
MAX = 32
};
int main(void)
{
char buf[MAX];
int x;
double y;
printf("x * 2 = ?: x = ");
fgets(buf,MAX,stdin);
x = atoi(buf);
printf("y + 1 = ?: y = ");
fgets(buf,MAX,stdin);
y = atof(buf);
printf("%d * 2 = %d\n", x, x*2);
printf("%f + 1 = %f\n", y, y+1);
return 0;
}
$ ./a.out
x * 2 = ?: x = 1234
y + 1 = ?: y = 1.234e-3
1234 * 2 = 2468
0.001234 + 1 = 1.001234
$
しかし、文字列の中に数値にできない文字があった場合、そこまでの文字列を数値化し、以降の文字列は捨てられます。この仕様のおかげで改行コードを無視できますが、なかなか厄介です。
$ ./a.out
x * 2 = ?: x = 12a34
y + 1 = ?: y = 1.234ee-3
12 * 2 = 24
1.234000 + 1 = 2.234000
$
atoi()
やatof()
のさらに厄介な点として、文字列の先頭が数字でない場合、「0」が返されます。つまり、整数としての「0」なのか、エラーの「0」なのか分かりません。そこで、入力された文字をチェックする関数が必要になります。概ね、次のような感じだと思います(実数は作るの大変なんで整数だけ)。
static const int isInteger(const char *s)
{
// 先頭が負号なら次の要素に進む
if (*s == '-') {
++s;
}
while (*s != '\n') { // \nを削って\0にした方が自然かも
if (*s >= '0' && *s <= '9') { // isdigit()でもよい
++s;
} else {
return 0;
}
}
return 1;
}
他にも、数字だけを抽出して整数型で返す方法も考えられます。数字がなかった場合、標準エラー出力されプログラムが終了します。for (;;) {}
の中でループさせるのが現実的かもしれません。
static const int strToInt(const char *s, int length)
{
int i = 0;
char buf[length]; // enumでもいい
// 負号に対応
if (*s == '-') {
buf[i] = *s;
++s;
++i;
}
// 数字なら配列に代入しカウンタを進める
while (*s != '\0') {
if (*s >= '0' && *s <= '9') {
buf[i] = *s;
++s;
++i;
} else {
++s;
}
}
i = 0;
if (buf[i] == '+' || buf[i] == '-') {
++i;
}
// 正負記号を除いた先頭文字が数字なら返す
if (buf[i] >= '0' && buf[i] <= '9') {
return atoi(buf);
} else {
fprintf(stderr, "NaN\n");
exit(1);
}
}
$ ./a.out
Type: - 1 2345asd6fbja7k8 9
-123456789
$ ./a.out
Type: abcdefg
NaN
$ echo $?
1
実数の場合は、「./e/E/e-/E-」などの文字の出現回数をチェックする必要があるので、ちょっと面倒かもしれません。実は文字列を数値に変換する関数として、strtod()
やstrtol()
といった高度なものもあります。これらは、数値として使えない文字をchar *endptr
に格納してくれますが、詳しくないので触れません。
いずれにせよ、この入力された文字列が想定したものであるかをしっかりとチェックする必要があります。また、次で述べるバッファ(ストリーム)をクリアする仕組みも必要です。
forループの中で使う
for (;;) {}
で、こちらが想定した入力がされるまでループさせます。次のコードは、入力された文字数がn字以内であればループを抜けて文字と文字数が出力されるものです。
注意点として、配列bufに入り切らなかった入力がバッファに残るので、これを解放する必要があります(このバッファはストリームと同義です)。ここでは実質的な解放を、getc(stdin)
でストリームを読み込むことで実現しています。fflush(stdin)
もよく使われますが、これは未定義動作です。
#include <stdio.h>
#include <string.h>
enum {
MAX = 10
};
int main(void) {
char buf[MAX];
for (;;) { // 規定の文字数以内を入力するまでループ
printf("Type within %d characters: ", MAX-2);
fgets(buf,MAX,stdin);
/* bufに改行コードがあればループを抜ける
* なければバッファを解放して再ループ */
if (strchr(buf, '\n')) {
break;
} else {
while (getc(stdin) != '\n');
// fflush(stdin); // 未定義動作
continue;
}
}
strtok(buf, "\n");
printf("%s is %d characters\n", buf, strlen(buf));
return 0;
}
$ ./a.out
Type within 8 characters: hello, world
Type within 8 characters: hello
"hello" is 5 characters
$
バッファを解放せずにcontinue
した場合、バッファに残っている文字列が勝手に入力されます。
$ ./a.out
Type within 8 characters: hello, world
Type within 8 characters: "rld" is 3 characters
$
関数化する?
文字列を取得する関数を作ってみます、といってもあまり実用性はないと思います。次のコードは入力された文字列をCSVにして表示するものです。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef int bool;
#ifndef TRUE
#define TRUE (0==0)
#define FALSE (!TRUE)
#endif
static char *getString(void);
static void disposeString(const char *p);
static void clearBuffer(void);
static bool isNameCharacter(const char *p);
enum {
MAX = 32
};
int main(void)
{
const int count = 3; // 入力する項目数
char *mes[] = {"Name", "Age", "Country"};
char str[128] = {'\0'};
char *p;
int i;
for (i = 0; i < count; i++) {
for (;;) { // 名前に使う文字が入力されるとループから抜ける
printf("%d/%d %s: ", i+1, count, mes[i]);
p = getString();
if (isNameCharacter(p)) {
break;
} else {
disposeString(p);
}
}
p[strlen(p)] = ','; // 行末にカンマを追加
strcat(str, p);
disposeString(p);
}
str[strlen(str)-1] = '\0'; // 行末のカンマを削除
puts("#Name,Age,Country");
printf("%s\n", str);
return 0;
}
static char *getString(void)
{
char *buf;
buf = malloc(MAX + 1);
for (;;) {
fgets(buf,MAX,stdin);
if (strchr(buf, '\n')) {
break;
} else {
clearBuffer();
}
}
strtok(buf,"\n");
return buf;
}
static void disposeString(const char *p)
{
free((char *)p);
}
static void clearBuffer(void)
{
while (getc(stdin) != '\n');
}
static bool isNameCharacter(const char *p)
{
while (*p != '\0') {
if (*p >= '0' && *p <= '9' ||
*p >= 'A' && *p <= 'Z' ||
*p >= 'a' && *p <= 'z' ||
*p == ' ' || *p == '-') {
++p;
} else {
return FALSE;
}
++p;
}
return TRUE;
}
$ ./make_csv
1/3 Name: Jean-Luc Godard
2/3 Age:
2/3 Age: 90
3/3 Country: ~*?
3/3 Conntry: France
#Name,Age,Country
Jean-Luc Godard,90,France
$
C言語には、文字列型がないので、malloc()
を使ってヒープ領域を使います。普通、malloc()
とfree()
は同じ階層で行いますが、今回は難しいのでfree()
するだけの関数を用意しました。
結局のところ、エラー処理をどこまでやるか、これが標準入力の、というよりもC言語でプログラムを書く上での重要な課題だと思います。たとえば、3つ目の質問ならば、0以上の整数だけを受け取る、といったように枚挙にいとまがありません。
標準入力は使わない?
業務レベルのプログラムを書いたことがないので分かりませんが、おそらく(キーボードによる)標準入力を使う場面はあまりないのでないか、と想像しています。というのも、コマンドライン引数からファイルを読み込んで処理した方が楽だからです。コマンドライン引数であれば、スクリプトで自動化しやすく、大量のデータを処理するのに向いていると思います。