LoginSignup
166
151

More than 5 years have passed since last update.

標準入力から安全に文字列を受け取る方法いろいろ

Last updated at Posted at 2014-11-09

予備知識

最初に, fgets scanf printf などに共通する テキストモード を確認しておきましょう。これは,OSごとに違う改行コード

OS 俗称 エスケープ表記
Windows CRLF \r\n
Linux
macOS
FreeBSD
       など
LF \n
Macintosh CR \r

これらすべてをC言語側からは \n として扱えるようにしてくれる特性を持ちます。便利ですね。

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

格納領域が次のように256文字ぶん用意されていると仮定します。

char buffer[256];

scanffgetsの主要な違いを簡単にまとめると以下のようになります。NULL文字\0が終端に自動付与されることを考慮すると,ユーザが実質入力できるのは255文字までであることに注意します。

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

scanf"%s"のようにサイズ指定せずに使うこともできますが,バッファオーバーランの危険性があるので出来るだけちゃんと書きましょう。

scanfの利用

scanfは多機能なので動作のカスタマイズが可能です。そのままの状態ではfgetsの劣化版のように見えてしまいますが,フォーマットをちゃんと書くことで実用性が向上します。

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

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

  • 1文字以上読み取れたときは 1
  • 改行だけが入力されたときは 0
  • 最初から Ctrl+D (Windowsの場合は Ctrl+Z) でEOFが入力されたときは EOF
    (この定数の値は -1)

失敗したときにはそのまま return 1; してプログラムを異常終了扱いにさせておくとよいでしょう。

必ず1文字以上入力させる場合
#include <stdio.h>

int main(void)
{
    char buffer[256];

    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(void)
{
    char buffer[256] = "";

    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を書きます)
#include <stdio.h>

int main(void)
{
    char buffer[256];
    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;
}

%n系は符号付き整数で受け取るように定義されていますが,負の値が出現する可能性がゼロなので,符号無し整数でも特に問題はありません)

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];
    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 を使うことが多いと思います。ここでは,複数個の数値が入力された際,余分なものは無視するように書いてみます。

scanf関数で直接整数として受け取る場合
#include <stdio.h>

int main(void)
{
    int num;

    printf("Input: ");
    if (scanf("%d%*[^\n]", &num) != 1) {
        return 1;
    }
    scanf("%*c");

    printf("Output: %d\n", buffer);
    return 0;
}

しかし,これには

  • 最初にいきなり空白文字が入力された場合に読み取りが終了しない
  • 範囲外の値が入力された場合に対応できない

などの欠点があります。

scanfstrtolの併用

scanf単独でやろうとすると上述のような欠点があるので,まず文字列として受け取って,あとから変換するアプローチを採ってみます。strtol関数を利用するので,整数の型は long (正確にはlong int) になります。なお,atoiatolにはエラー検出機能は無いので注意してください。

strtol関数で文字列から変換する場合

(簡易版)
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main(void)
{
    char buffer[32], *endptr;
    long num;

    printf("Input: ");
    if (scanf("%31[^\n]%*[^\n]", buffer) != 1) {
        return 1;
    }
    scanf("%*c");

    num = strtol(buffer, &endptr, 10);
    if (*endptr != '\0' || errno == ERANGE) {
        return 1;
    }

    printf("Output: %ld\n", num);
    return 0;
}
strtol関数で文字列から変換する場合

(エラーメッセージも表示する完全版)
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>

int main(void)
{
    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文字以外の文字をすべて読み取ることができます。つまり,改行も読み取れるのです。

確認用にstrtok関数を利用して行ごとに番号を付けて表示
#include <stdio.h>
#include <string.h>

int main(void)
{
    char buffer[4096], *p;
    size_t i = 0;

    printf("Input: \n");
    if (scanf("%4095[\x01-\xff]", buffer) != 1) {
        return 1;
    }

    printf("Output: \n");
    for (i = 0, p = strtok(buffer, "\n"); p; ++i, p = strtok(NULL, "\n")) {
        printf("[%zu] %s\n", i, p);
    }

    return 0;
}

任意サイズの1行を受け取る

基本的にC言語で可変長は「どうしても」という場合以外は避けるべきですが,メモリが許す限りどんな長さの1行でも受け取ってくれるgetlineという関数があります。使用後に自分でメモリをfreeする必要があるので注意してください。

改行だけの入力を認めず,改行を削除する場合
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    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;
}

ここでは最初に確保される領域の大きさを 4096 文字にしました。オーバーした場合は自動的に拡張されますが,大量に入力されることが事前にわかっていれば大きめに設定しておくほうが望ましいです。

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

基本事項

文字コードについて

さて,ここまではcharを文字と称してきましたが,厳密にはこれは誤りです。なぜならば,日本語などはchar1個ぶんのデータでは表せないからです。例えば最近最も使われるUTF-8という文字コードだと,日本語で使われる1文字にはchar3つ分の領域が必要です。そのためchar1つをバイトという単位で数え,日本語で使われる文字はそれらが複数なのでマルチバイト文字と呼ばれます。

マルチバイト文字を扱う際,連続したバイト列としてとして扱うぶんには何も工夫しなくても困らないことが多いのですが,1バイトではなく1文字ずつ取り出したいあるいはバイト長ではなく文字列長を求めたいというときには特殊な処理を書く必要があります。

ロケール設定

マルチバイト文字を正しく扱う場合,以下の設定を行います。

ヘッダのインクルード
#include <locale.h>
setlocale関数の実行
setlocale(LC_ALL, "");

使用する型など

  • マルチバイト文字を1文字ずつ区別できるように格納する場合,charの代わりにwchar_tという型を使います。wchar.hのインクルードが必要です。
  • charの代わりにwchar_tのリテラルを書きたい場合, '' "" はそれぞれ L'' L"" と書きます。
  • charの代わりにwchar_t用のフォーマット指定子を使いたい場合, %c %s はそれぞれ %lc %ls と書きます。

入力された文字列を1文字ずつ「」で括って表示する

(%nで求められるのはバイト長であって文字列長ではありません,そのためループ条件変数としては使えません)
#include <stdio.h>
#include <wchar.h>
#include <locale.h>

int main(void)
{
    wchar_t buffer[256];
    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;
}

macOS における scanf 関数および wscanf 関数のバグ

以下に述べることはmacOS固有です。Linuxでは確認できませんでした。WindowsやFreeBSDでは未確認です,誰か教えてください。

  • scanf関数で改行のみを入力した場合,%255l[^\n]のように指定しても改行が1文字として含まれてしまうというバグがあります。

    • これは次に述べる,L""を第1引数に渡せる版のwscanf関数では起きません。
  • しかし,wscanf関数にもまた別のバグがあります。%ls文字列だけを読み取ろうとしているときに最初から Ctrl+D でEOFが入力された場合,wscanf関数はEOFを返さずに 0 を返します

    • 「改行だけが入力された場合」と「最初からEOFが入力された場合」を区別することはできません。
166
151
12

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
166
151