C言語の基礎 - 標準入力から安全に文字列を取得する

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

指定サイズ内で1行を受け取る

次のように、文字列の格納領域が256[byte]用意されているとする。

 char buffer[256] = {'\0'};


scanf関数とfgets関数の主な違いを纏めると以下のようになる。
NULL文字\0が終端に自動付与されることを考慮すると、ユーザが実質入力できるのは255[byte]までである。

※注意
scanf関数はscanf("%s", buffer);のようにサイズを指定せずに使用できるが、バッファオーバーランの危険性があるので避けること。

説明 scanf関数 fgets関数
基本的な使用法 scanf("%255s", buffer) fgets(buffer, 256, stdin)
改行の扱い 改行文字の直前まで読み込む 改行文字も一緒に読み込む
成功時の戻り値 読み込んだパラメータ数 buffer
失敗時の戻り値 0
または
EOF (-1)
NULL
入力文字の取得 できる できない



scanf関数の利用

scanf関数は多機能であり、フォーマットを記述することで動作のカスタマイズが可能である。

以下に、scanf関数のフォーマットを記載する。

  • %sは全ての空白文字(半角スペースも含む)を無視するため、%sの代わりに%[^\n]と指定することで、無視する対象を改行だけに限定できる。
    例えば、%255[^\n]は、改行以外の文字列を1〜255文字読み込むことを意味する。
  • %の代わりに%*を使用すると、その部分を読み飛ばすことができる。
    例えば、256文字以上入力する場合、最初の255文字までを受け取り、残りを捨てる場合は、%255[^\n]%*[^\n]と記述する。
  • 再度scanf関数を使用する場合に備えて、残った改行は%*cで読み飛ばす。
    これにより、保持されていた入力データは全て処理されたことになる。
    ただし、次に読み取るものが%d等の数値である場合には、%*cで読み飛ばさなくても特に影響はない。


また、scanf関数は、正常に読み取れた場合は変数に格納したパラメータの数を返すため、この数を確認して成功したかどうか判定することができる。
例えば、%255[^\n]%*[^\n]を実行した場合の戻り値は、以下に示すようになる。

  • 1文字以上の読み込みに成功した場合は、1
  • 改行のみが入力された場合は、0
  • 未入力の状態において、[Ctrl] + [D]キー (Linux) または [Ctrl] + [Z]キー (Windows)でEOFが入力された時は、EOF (EOF定数の値 : -1)
  • また、読み込みに失敗した場合は、return 1;等としてプログラムを異常終了扱いにするとよい。


%255[^\n]%*[^\n]の場合

  • %255
    最大255文字まで読み込む。
  • [^\n]
    改行以外の文字を読み込む。
  • %*[^\n]
    改行以外の残りの文字を読み込むが破棄する。
    バッファオーバーフロー防止


例えば、"aaa\nbbb\nccc\nddd"と入力された場合、変数には"aaa"のみが格納される。
これは、[^\n]は最初の改行文字で読み込みを停止、残りの"bbb\nccc\nddd"は%*[^\n]で破棄されるためである。
これにより、1行のみを安全に読み込むことが可能となる。

以下の例では、必ず1文字以上入力させている。

 #include <stdio.h>
 
 int main()
 {
    char buffer[256] = {'\0'};
 
    printf("Input: ");
    if (scanf("%255[^\n]%*[^\n]", buffer) != 1)
    {
       return 1;
    }
 
    scanf("%*c");
 
    printf("Output: %s\n", buffer);
 
    return 0;
 }


以下の例では、改行だけの入力を認めている。

 #include <stdio.h>
 
 int main()
 {
    char buffer[256] = {'\0'};
 
    printf("Input: ");
    if (scanf("%255[^\n]%*[^\n]", buffer) == EOF)
    {
       return 1;
    }
    scanf("%*c");
 
    printf("Output: %s\n", buffer);
 
    return 0;
 }


以下の例では、入力された文字数も取得している。(%255[^\n]の後ろは、unsigned / intの場合は%n、size_t / ssize_tの場合は%znを指定する)
(%n系は符号付き整数で取得するように定義されているが、負の値が出現する可能性がゼロなので、符号無し整数でも問題ない)

 #include <stdio.h>
 
 int main(void)
 {
    char buffer[256] = {'\0'};
    size_t length;
 
    printf("Input: ");
    if (scanf("%255[^\n]%zn%*[^\n]", buffer, &length) != 1)
    {
       return 1;
    }
    scanf("%*c");
 
    printf("Output: %s\n", buffer);
    printf("Length: %zu\n", length);
 
    return 0;
 }



fgets関数の利用

fgets関数は、改行も1行に含めて一緒に取得する。
削除する場合は、各自でコードを記述しなければならない。また、strlen関数を使用するなどして、文字列長を求めることも必要である。

また、戻り値は以下のようになる。

  • 1文字以上読み取れた場合は、buffer(改行だけでも1文字以上という扱いになる)
  • 最初から[Ctrl] + [D](Windowsの場合は[Ctrl] + [Z])でEOFが入力された場合は、NULL
  • 失敗した場合は、return 1;としてプログラムを異常終了扱いにさせるとよい。


以下の例では、必ず1文字以上入力させて改行を削除している。

 #include <stdio.h>
 #include <string.h>
 
 int main(void)
 {
    char buffer[256] = {'\0'};
    size_t length;
 
    printf("Input: ");
    if (fgets(buffer, 256, stdin) == NULL || buffer[0] == '\n')
    {
       return 1;
    }
    length = strlen(buffer);
    if (buffer[length - 1] == '\n')
    {
       buffer[--length] = '\0';
    }
 
    printf("Output: %s\n", buffer);
    printf("Length: %zu\n", length);
 
    return 0;
 }


以下の例では、改行だけの入力を認めて改行を削除している。

 #include <stdio.h>
 #include <string.h>
 
 int main(void)
 {
    char buffer[256];
    size_t length;
 
    printf("Input: ");
    if (fgets(buffer, 256, stdin) == NULL)
    {
       return 1;
    }
    length = strlen(buffer);
    if (length > 0 && buffer[length - 1] == '\n')
    {
       buffer[--length] = '\0';
    }
 
    printf("Output: %s\n", buffer);
    printf("Length: %zu\n", length);
 
    return 0;
 }


以下の例では、改行だけの入力を認めて改行を削除している。

 #include <stdio.h>
 
 int main(void)
 {
    char buffer[256];
 
    printf("Input: ");
    if (fgets(buffer, 256, stdin) == NULL)
    {
       return 1;
    }
 
    printf("Output: %s\n", buffer);
 
    return 0;
 }



整数の読み取り

scanf関数の使用

整数を受け取る場合、一般的にscanf関数の%dを使用することが多い。

以下の例では、複数個の数値が入力された時、2個目以降は無視している。
ただし、以下のような欠点が存在する。

  • 最初が空白文字の場合、scanf関数が終了しない。
  • 範囲外の値が入力された場合、対応できない。
 #include <stdio.h>
 
 int main()
 {
    int num;
 
    printf("Input: ");
    if (scanf("%d%*[^\n]", &num) != 1)
    {
       return 1;
    }
    scanf("%*c");
 
    printf("Output: %d\n", buffer);
    return 0;
 }


scanf関数とstrtol関数の使用

scanf関数のみでは上記のような欠点があるため、文字列として読み取り整数に変換する方法がある。

文字列から整数に変換するにはstrtol関数を使用するため、整数はlong型とする。
なお、atoi関数やatol関数にはエラー検出機能は無いので使用しないこと。

 #include <stdio.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <limits.h>
 
 int main()
 {
    char buffer[32], *endptr;
    long num;
 
    // 文字列として読み取る
    printf("Input: ");
    if (scanf("%31[^\n]%*[^\n]", buffer) != 1)
    {
       fprintf(stderr, "Error: No input specified\n");
       return 1;
    }
    scanf("%*c");
 
    // 文字列から整数に変換する
    num = strtol(buffer, &endptr, 10);
    if (*endptr != '\0')
    {
       fprintf(stderr, "Error: Invalid charcter found: %c\n", *endptr);
       return 1;
    }
 
    if (errno == ERANGE)
    {
       fprintf(stderr, "Error: Out of range (%s)\n", num == LONG_MAX ? "Overflow" : "Underflow");
       return 1;
    }
 
    printf("Output: %ld\n", num);
 
    return 0;
 }



指定サイズ内で複数行を読み取る

fread関数を使用する場合、行という概念にとらわれず,格納領域が全て埋まる、または、入力がEOFに到達するまで読み取ることができる。
ただし、バイナリモードで読み取るため改行コードの自動変換の機能が無い。そのため、テキストを処理する場合は不便である。

scanf関数では、%4095[\x01-\xff]のように記述することにより、NULL文字以外の文字を全て読み取ることができる。(改行も読み取ることができる)

 #include <stdio.h>
 #include <string.h>
 
 int main()
 {
    char buffer[4096] = {0},
         *p = NULL;
 
    printf("Input: \n");
    if (scanf("%4095[\x01-\xff]", buffer) != 1)
    {
       return 1;
    }
 
    printf("Output: \n");
    for (size_t i = 0, p = strtok(buffer, "\n"); p; i++, p = strtok(NULL, "\n"))
    {  // 確認用にstrtok関数を使用して行ごとに番号を付加して表示する
       printf("[%zu] %s\n", i, p);
    }
 
    return 0;
 }



任意のサイズの1行を読み取る

C言語において、可変長は基本的に避けるべきであるが、メモリが許容する限りどのような長さの1行でも読み取るgetline関数が存在する。
ただし、使用後は手動でメモリを解放する必要があることに注意する。

以下の例では、改行のみの入力を認めずに、改行を削除している。
最初に確保される領域の大きさを4096文字にしている。
サイズがオーバーする場合は自動的に拡張されるが、事前に多くの文字を入力することが分かっている場合、大きめに設定しておくほうが望ましい。

 #include <stdio.h>
 #include <stdlib.h>
 
 #define _GNU_SOURCE
 
 int main()
 {
    char *buffer = NULL;
    ssize_t length;
 
    printf("Input: ");
    if ((length = getline(&buffer, &(size_t){4096}, stdin)) == EOF)
    {
       return 1;
    }
 
    if (buffer[length - 1] == '\n')
    {
       buffer[--length] = '\0';
    }
 
    printf("Output: %s\n", buffer);
    printf("Length: %zd\n", length);
 
    free(buffer);
 
    return 0;
 }



日本語の文字列を文字単位で処理する

文字コードについて

例えば、マルチバイトにおいて、UTF-8では日本語で使用する1文字には3バイトの領域が必要である。
マルチバイト文字では、連続したバイト列としてとして扱う場合は特に問題無いが、1文字ずつ取り出す場合や文字列長を求める場合は特殊な処理を記述する必要がある。

ロケール設定

マルチバイト文字を正しく扱う場合、以下の関数を使用する。

 #include <locale.h>
 
 setlocale(LC_ALL, "");


使用するデータ型

マルチバイト文字を1文字ずつ区別できるように格納する場合、wchar_t型を使用する。
wchar_t型のリテラルを記述する場合、L'<1文字>'およびL"<文字列>"と記述する。

また、wchar_t型のフォーマット指定子を使用する場合、1文字の時は%lc、文字列の時は%lsと記述する。

サンプルコード

以下の例では、入力された文字列を1文字ずつ鍵括弧で括って表示している。
フォーマット指定子において、%nで求めることができるのはバイト長であり文字列長ではないため、ループ条件の変数としては使用できない。

 #include <stdio.h>
 #include <wchar.h>
 #include <locale.h>
 
 int main()
 {
    wchar_t buffer[256] = {0};
    setlocale(LC_ALL, "");
 
    printf("入力: ");
    if (scanf("%255l[^\n]%*[^\n]", buffer) != 1)
    {
       return 1;
    }
    scanf("%*c");
 
    printf("出力: ");
    for (wchar_t *c = buffer; *c; c++)
    {
       printf("「%lc」", *c);
    }
    putchar('\n');
 
    return 0;
 }