作曲・浄書・指導・音響

金沢音楽制作

金沢音楽制作では、楽曲・楽譜の制作と、作曲や写譜などレッスンを行っています。

gets()やscanf()を使わずに標準入力する

本記事は、C言語で簡単なプログラムを書ける程度の知識を持っていることを前提として書いてあります。また、筆者はプログラマではありませんし、理系でもありません。内容に誤りがあったり、仕様から外れた未定義動作があるかもしれません。なお、C89(90)を想定しているつもりです。

C言語で標準入力する場合、fgets()を使い、必要に応じて加工します。市販のテキストでは、文字列の取得にgets()、数値の取得にscanf()が用いられていますが、これらにはバッファオーバーフローや想定外の入力といった危険性があるので基本的に使われません。ただし、fgets()を使えば大丈夫、というものでもありません。

fgets()とは

fgets()は、行を単位として文字列を取得する関数です。キーボード入力での改行コードも取得されます。stdin.hが必要です。

#include <stdio.h>
char *fgets(char *buf, int n, FILE *fp)

fgets()には、3つの引数が必要です。1つ目は読み込んだ文字列を格納する配列、2つ目は格納できる文字列の長さ、3つ目はストリーム(stdin)です。細かい仕様は専門書にまかせるとして、実際に使ってましょう。

#include <stdio.h>

int main(void)
{
  const int Maxsize = 32;
  char buf[Maxsize];

  printf("Type: ");
  fgets(buf,Maxsize,stdin);

  puts(buf);

  return 0;
}
$./fgets
Type: hello, world
hello, world

$

第1引数にchar型の配列bufの先頭アドレスを、第2引数に定数で定義したMaxsize(=32バイト)を、そして第3引数にstdinが指定されています。この中で注意が必要なのは、第2引数です。

第2引数のバッファサイズは、入力する文字列+改行コード+null文字、つまり文字列+2バイト分のサイズが必要になります。puts()の改行が二重に出力されているのはそのためです。また、バッファサイズを超えて入力してしまった場合、サイズ-1以降の文字列は削られ、最後の要素がnullターミネートされます。

#include <stdio.h>
#include <string.h>

int main(void)
{
  const int Maxsize = 10;
  char buf[Maxsize];
  int i;

  printf("Type: ");
  fgets(buf,Maxsize,stdin);

  printf("Array size   : %d\n", sizeof buf / sizeof(char));
  printf("String length: %d\n", strlen(buf));

  for (i = 0; i < Maxsize; i++) {
    printf("%2d: %c\n", i+1, buf[i]);
  }

  return 0;
}
$ ./fgets
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()の基本的な動作です。前述したよう、文字列に入力時の改行コードが含まれています。この改行コードは邪魔なので、削り方を次で紹介します。

改行コードを削る

改行コードを削る方法は、筆者が知る限り二通りあります。一つはstrtok()を使ってトークンに分解する方法、他方は改行コードのアドレスにnullを代入する方法です(null文字から一つ前のアドレス)。いずれもstring.hが必要です。なお、数値にする場合はこの作業はしなくても大丈夫です(本当に無視してよいのか分かりません)。

#include <stdio.h>
#include <string.h>

int main(void)
{
  const int Maxsize = 32;
  char buf[Maxsize];

  // トークンに分解する方法
  printf("Type: ");
  fgets(buf,Maxsize,stdin);
  strtok(buf,"\n");  // 書くのが簡単
  puts(buf);

  // nullを代入する方法
  printf("Type: ");
  fgets(buf,Maxsize,stdin);
  buf[strlen(buf)-1] = '\0';  // C言語っぽい
  puts(buf);

  return 0;
}
$ ./fgets
Type: hello,
hello,
Type: world
world
$

文字列から改行コードが消えて、puts()による改行のみが確認できます。なお、連続してfgets()を使う場合、その都度バッファ(ストリーム)を解放する必要があります。これについては、「forループの中で使う」で詳述します。

数値に変換する

fgets()で取得したのは文字列ですから、このままでは計算ができません。そこで、文字列を数値に変換します。方法はいくつかありますが、よく使うものとして、atoi()atof()があげられます。それぞれ、Ascii to Integer、Ascii to Floatの略です。なお、atof()の戻りの型は、double型で指数表記にも対応しています。stdlib.hが必要です。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  const int Maxsize = 32;
  char buf[Maxsize];
  int    x;
  double y;

  printf("x * 2 = ?: x = ");
  fgets(buf,Maxsize,stdin);
  x = atoi(buf);

  printf("y + 1 = ?: y = ");
  fgets(buf,Maxsize,stdin);
  y = atof(buf);

  printf("%d * 2 = %d\n", x, x*2);
  printf("%f + 1 = %f\n", y, y+1);

  return 0;
}
$ ./fgets
x * 2 = ?: x = 1234
y + 1 = ?: y = 1.234e-3
1234 * 2 = 2468
0.001234 + 1 = 1.001234
$

しかし、文字列の中に数値にできない文字があった場合、そこまでの文字列を数値化し、以降の文字列は捨てられます。この仕様のおかげで改行コードを無視できますが、なかなか厄介です。

$ ./fgets
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);
  }
}
$ ./fgets
Type: - 1 2345asd6fbja7k8  9
-123456789
$ ./fgets
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>

int main(void) {
  const int Maxsize = 10;
  char buf[Maxsize];

  for (;;) {  // 規定の文字数以内を入力するまでループ
    printf("Type within %d characters: ", Maxsize-2);
    fgets(buf,Maxsize,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;
}
$ ./fgets
Type within 8 characters: hello, world
Type within 8 characters: hello
"hello" is 5 characters
$

バッファを開放せずにcontinueした場合、バッファに残っている文字列が勝手に入力されます。

$ ./fgets
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);

int main(void)
{
  const int InputCount = 3;  // 入力する項目数 
  char mes[][16] = {"Name", "Age", "Country"};
  char str[128] = {'\0'};
  char *p;
  int i;

  for (i = 0; i < InputCount; i++) {
    for (;;) {  // 名前に使う文字が入力されるとループから抜ける
      printf("%d/%d %s: ", i+1, InputCount , 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)
{
  const int Maxsize = 32;
  char *buf;

  buf = malloc(Maxsize + 1);

  for (;;) {
    fgets(buf,Maxsize,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以上の整数だけを受け取る、といったように枚挙にいとまがありません。

標準入力は使わない?

業務レベルのプログラムを書いたことがないので分かりませんが、おそらく標準入力を使う場面はあまりないのでないか、と想像しています。というのも、コマンドライン引数からファイルを読み込んで処理した方が楽だからです。コマンドライン引数であれば、スクリプトで自動化しやすく、大量のデータを処理するのに向いていると思います。

  • 公開日:2021-09-30
  • 更新日:2021-10-12