第21回 | C言語 scanf の戻り値とエラー処理|入力失敗(0/EOF)を検出し安全に再入力する | 実践編1| UNIX Cafe

* 当サイトでは、コンテンツの一部に広告を掲載しています。

System Note $ cat /proc/ai-disclosure

本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。

実践編1|C言語でscanf のエラー処理を丁寧にする:入力失敗を確認して安全に処理する

はじめてのC言語 | 第21回

目次

はじめに

要点:scanf は「読み取りに成功した項目数」を返します。整数を1個読めれば 1、型が合わなければ 0、変換に成功する前に入力の終わりや読み取りエラーになった場合は EOF を返します。だから scanf("%d", &x) != 1 と書けば、整数を1個読み取れなかった場合を検出できます。

第20回では、入力、配列、関数、構造体、ファイル入出力を組み合わせた総合演習を行いました。
今回は、その総合演習を少し実用的にするために、scanf のエラー処理を丁寧にします。

scanf は便利ですが、入力が想定どおりでない場合に失敗することがあります。
その失敗を確認せずに処理を続けると、意図しない値を使ってしまう可能性があります。

この回の目的は次の6点です。

  • scanf の戻り値の意味を理解する
  • 整数入力に失敗した場合の処理を書く
  • 点数の範囲チェックを行う
  • 大きすぎる数を入力した場合の注意点を知る
  • 関数の戻り値で成功と失敗を表す
  • 第20回の総合演習を安全な形に改良する

scanf の戻り値の意味(1・0・EOF の違い)

C言語 scanf の戻り値とエラー処理|入力失敗(0/EOF)を検出し安全に再入力する | 実践編1| UNIX Cafe
scanf の戻り値の意味(1・0・EOF の違い)

scanf は、読み取りに成功した項目数を返します。

ただし、常に成功数だけが返るわけではありません。
たとえば整数を読もうとして文字が来た場合は 0、変換に成功する前に入力の終わりや読み取りエラーになった場合は EOF が返ります。

戻り値を 1 か、そうでないか、だけでなく、0EOF に分けて見ると、失敗の理由を考えやすくなります。

int r = scanf("%d", &score);

if (r == 1) {
    /* 整数を1個読み取れた */
} else if (r == 0) {
    /* 型が合わない(例: abc を入力した) */
} else {
    /* r == EOF:入力の終わり(Ctrl+D など) */
}

型が合わないときは 0、入力そのものが尽きたときは EOF です。多くの場合は != 1 でまとめて失敗扱いにできますが、再入力させたいのか、終了したいのかで処理を分けたいときに、この区別が役立ちます。

たとえば次のコードでは、整数を1個読み取れた場合に 1 が返ります。

int score;

if (scanf("%d", &score) == 1) {
    printf("success\n");
}

入力が先頭から整数として読めない場合、scanf("%d", &score)1 を返しません。
そのため、戻り値を確認することで入力エラーを検出できます。

なお、読み取りに失敗した文字は、その場で自動的に消えるとは限りません。
たとえば abc を入力して %d が失敗した場合、その文字が入力に残るため、同じまま再び scanf を呼ぶと連続で失敗することがあります。

まずは整数入力を確認する

ソースコード

scanf_check.c という名前で保存します。

#include <stdio.h>

int main(void) {
    int score;

    printf("score: ");
    if (scanf("%d", &score) != 1) {
        printf("input error\n");
        return 1;
    }

    printf("%d\n", score);

    return 0;
}

実行手順

1. 作業ディレクトリに移動する

cd ~/Desktop

2. コンパイルする

clang scanf_check.c -o scanf_check

3. 実行する

./scanf_check

実行例:

score: 80
80

整数として読み取れない文字(例: abc)を入力した例:

ここでの「整数以外」とは、abc のように先頭が数字以外の文字で始まる入力のことです。この場合、scanf は整数を1個も読み取れないため 1 を返さず、input error になります。

score: abc
input error

一方で、0.1 のように先頭が数字の入力は注意が必要です。%d は整数として読める 0 までを読み取って成功するため、input error にはならず、0 が表示されます(残りの .1 は入力に残ります)。
つまりこのプログラムで検出できるのは「先頭から整数として読めない入力」です。小数や余分な文字が後ろに付く入力までは、この方法だけでは検出できません。

score: 0.1
0

コードの読み方

scanf(“%d”, &score) != 1

if (scanf("%d", &score) != 1) {
    printf("input error\n");
    return 1;
}

%d は整数を1個読み取る指定です。
整数を正しく読み取れた場合、scanf1 を返します。

1 でない場合は、少なくとも「整数を1個読み取れなかった」と判断できます。

return 1

return 1;

main 関数で 0 以外を返すと、プログラムが正常終了ではなかったことを表せます。
この例では、入力エラーが起きたため 1 を返しています。

入力に失敗したらやり直す(バッファのクリア)

先ほど、読み取りに失敗した文字は入力に残ると説明しました。
そのため、失敗してもそのまま scanf を呼び直すと、同じ文字でまた失敗し、止まったように見えることがあります。

正しい入力が来るまでやり直したいときは、残った文字を一度読み捨ててから、もう一度入力を促します。
ただし、EOF は「これ以上入力がない」という意味なので、再入力させずに終了するのが自然です。

#include <stdio.h>

/* 入力に残った文字を、改行(または入力終了)まで読み捨てる */
void clear_input(void) {
    int c;
    while ((c = getchar()) != '\n' && c != EOF) {
        /* 何もしない */
    }
}

int main(void) {
    int score;
    int r;

    while (1) {
        printf("score: ");
        r = scanf("%d", &score);

        if (r == 1) {
            break;
        }

        if (r == EOF) {
            printf("入力を終了します\n");
            return 1;
        }

        clear_input();
        printf("整数を入力してください\n");
    }

    printf("%d\n", score);

    return 0;
}

整数以外を入力しても止まらず、正しい整数が入るまで繰り返し尋ねます。Ctrl+D などで入力が終了した場合は、そこで処理を終えます。

score: abc
整数を入力してください
score: xyz
整数を入力してください
score: 80
80

getchar で1文字ずつ読み捨てる

while ((c = getchar()) != '\n' && c != EOF);

getchar は1文字ずつ読み取る関数です。この1行では、改行 '\n' に達するまで読み続けることで、入力に残った余分な文字をすべて捨てられます。EOF も条件に入れているのは、入力が尽きたときに無限ループにならないようにするためです。

点数の範囲を確認する

整数として読み取れても、値が適切とは限りません。
点数として扱うなら、たとえば 0 から 100 の範囲に制限できます。

#include <stdio.h>

int main(void) {
    int score;

    printf("score: ");
    if (scanf("%d", &score) != 1) {
        printf("input error\n");
        return 1;
    }

    if (score < 0 || score > 100) {
        printf("score must be between 0 and 100\n");
        return 1;
    }

    printf("%d\n", score);

    return 0;
}

実行例:

score: 120
score must be between 0 and 100

大きすぎる数を入力した場合

ここまでの範囲チェックでは、120 のように「整数としては読めるけれど、点数としては範囲外」という入力を扱いました。

一方で、次のように int に収まらないほど大きな数を入力した場合は、別の注意が必要です。

score: 999999999999999999999999

scanf("%d", &score) は、int 型の変数に整数を読み込む指定です。入力された数が int の範囲を超える場合、環境によって結果が変わることがあり、戻り値だけで安全に判定するのは難しくなります。

つまり、scanf の戻り値確認は「整数として読み取れたか」を確認するための基本ですが、「その数が型の範囲に安全に収まったか」まで厳密に扱うには向いていません。

このような範囲外の入力まで丁寧に扱いたい場合は、まず文字列として1行読み取り、あとから数値に変換する方法がよく使われます。たとえば fgets で入力を受け取り、strtol で数値に変換すると、変換失敗や範囲外をより細かく確認できます。

ただし、最初からすべてを覚える必要はありません。まずは scanf の戻り値を確認し、次に点数などの意味上の範囲を確認する、という順番で理解していけば大丈夫です。

入力用の関数を作る

入力処理を関数に分けると、main 関数を読みやすくできます。
成功したら 1、失敗したら 0 を返す関数にします。

#include <stdio.h>

int input_score(int *score) {
    printf("score: ");
    if (scanf("%d", score) != 1) {
        return 0;
    }

    if (*score < 0 || *score > 100) {
        return 0;
    }

    return 1;
}

int main(void) {
    int score;

    if (input_score(&score) == 0) {
        printf("input error\n");
        return 1;
    }

    printf("%d\n", score);

    return 0;
}

int *score

int input_score(int *score)

関数の中で入力結果を呼び出し元の変数に入れるため、ポインタで受け取っています。
scanf に渡すときは、すでにアドレスを受け取っているため score と書きます。

scanf("%d", score)

第20回の総合演習を改良する

ここからは、第20回の総合演習を入力エラーに対応した形にします。

改良版ソースコード

final_exercise_checked.c という名前で保存します。

#include <stdio.h>

struct Student {
    char name[20];
    int score;
};

int input_students(struct Student students[], int size) {
    int i;

    for (i = 0; i < size; i++) {
        printf("name: ");
        if (scanf("%19s", students[i].name) != 1) {
            return 0;
        }

        printf("score: ");
        if (scanf("%d", &students[i].score) != 1) {
            return 0;
        }

        if (students[i].score < 0 || students[i].score > 100) {
            return 0;
        }
    }

    return 1;
}

void print_students(struct Student students[], int size) {
    int i;

    for (i = 0; i < size; i++) {
        printf("%s %d\n", students[i].name, students[i].score);
    }
}

int sum_scores(struct Student students[], int size) {
    int i;
    int sum = 0;

    for (i = 0; i < size; i++) {
        sum += students[i].score;
    }

    return sum;
}

int save_result(struct Student students[], int size, int sum, double average) {
    FILE *fp;
    int i;

    fp = fopen("result.txt", "w");
    if (fp == NULL) {
        return 0;
    }

    for (i = 0; i < size; i++) {
        fprintf(fp, "%s %d\n", students[i].name, students[i].score);
    }

    fprintf(fp, "sum %d\n", sum);
    fprintf(fp, "average %.1f\n", average);

    fclose(fp);

    return 1;
}

int main(void) {
    struct Student students[3];
    int sum;
    double average;

    if (input_students(students, 3) == 0) {
        printf("input error\n");
        return 1;
    }

    print_students(students, 3);

    sum = sum_scores(students, 3);
    average = (double)sum / 3;

    printf("sum: %d\n", sum);
    printf("average: %.1f\n", average);

    if (save_result(students, 3, sum, average) == 0) {
        printf("file open error\n");
        return 1;
    }

    return 0;
}

改良版の実行手順

1. コンパイルする

clang final_exercise_checked.c -o final_exercise_checked

2. 実行する

./final_exercise_checked

正常な入力例:

name: Alice
score: 80
name: Bob
score: 90
name: Carol
score: 70
Alice 80
Bob 90
Carol 70
sum: 240
average: 80.0

範囲外の点数を入力した例:

name: Alice
score: 120
input error

改良した点

input_students の戻り値

int input_students(struct Student students[], int size)

第20回では void でしたが、今回は int にしています。
入力に成功した場合は 1、失敗した場合は 0 を返します。

save_result の戻り値

int save_result(struct Student students[], int size, int sum, double average)

ファイル保存も失敗する可能性があります。
そのため、保存に成功したら 1、失敗したら 0 を返す形にしています。

main 関数で失敗を確認する

if (input_students(students, 3) == 0) {
    printf("input error\n");
    return 1;
}

関数の戻り値を確認し、失敗した場合は処理を中止します。
これにより、不正な入力のまま計算や保存を続けないようにできます。

初心者がつまずきやすい点

scanf の戻り値を入力値そのものだと思ってしまう

scanf の戻り値は、入力された値ではありません。
読み取りに成功した項目数です。

if (scanf("%d", &score) != 1)

この例では、整数を1個読み取れたかどうかを確認しています。

文字列入力のサイズ制限を忘れる

次のように書くと、入力が長すぎる場合に危険です。

scanf("%s", students[i].name);

Visual Studio では、scanf を使うと安全性に関する警告が出ることがあります。これは scanf の戻り値そのものが悪いという意味ではありません。特に scanf("%s", 配列) のような文字列入力で、入力文字数を制限しないと配列の外まで書き込む危険があるためです。

たとえば、次のような配列を用意したとします。

char name[8];
int score = 100;

char name[8] に保存できるのは、最後の '\0' も含めて8文字分です。つまり、文字として入力できるのは最大7文字です。ここに TanakaTaro のような長い名前を %s でそのまま読み込むと、name に入りきらない文字まで書き込もうとします。

このように、用意した配列の範囲を超えて書き込んでしまうことを、バッファオーバーラン、またはバッファオーバーフローと呼びます。メモリ上では、配列の近くに score のような別の変数や、プログラムの管理情報が置かれている場合があります。配列の外へ書き込むと、それらを上書きしてメモリを壊してしまう可能性があります。

  • 別の変数の値が勝手に変わる
  • プログラムが途中で異常終了する
  • 関係なさそうな場所でエラーが出る
  • 実行するたびに結果が変わる

バッファオーバーランが起きた時点で、C言語としては未定義動作になります。「必ずこのように壊れる」と決まっているわけではなく、たまたま動くこともあれば、すぐに異常終了することもあります。だから、初心者が文字列入力で scanf を使うときは特に注意が必要です。

name が20文字分の配列なら、次のように最大文字数を指定します。

scanf("%19s", students[i].name);

最後の '\0' の分を残すため、20 ではなく 19 を指定します。

Visual Studio では scanf_s の使用をすすめられることもあります。scanf_s で文字列を読む場合は、配列のサイズも渡す必要があります。ただし、環境によって書き方が変わるため、この連載ではまず標準的な scanf の戻り値確認と、%19s のような幅指定を扱います。

また、%s は空白、タブ、改行などの空白文字までを1つの単語として読み取ります。そのため、Taro Yamada のように空白を含む名前をそのまま全部読み取りたい場合には、この方法だけでは足りません。1行まるごと読み取りたい場合は、fgets を使う方法を検討します。

もう1つ注意したいのは、char name[20]20 は、見た目の文字数ではなく、保存できるバイト数として考える必要がある点です。英数字は多くの環境で1文字1バイトですが、日本語などの文字は文字コードによって2バイトや3バイト以上になることがあります。そのため、%19s と書いても「日本語を19文字まで読める」という意味にはなりません。

エラーが起きても処理を続けてしまう

入力に失敗した場合は、計算やファイル保存を続けないようにします。
失敗を検出したら、メッセージを表示して return 1; で終了するのが分かりやすい書き方です。

よくあるエラー

整数以外を入力すると失敗を繰り返す

原因: %d で整数を読み取ろうとして失敗しています。
対処: 戻り値を確認し、失敗した場合はエラーメッセージを表示します。再入力させる場合は、入力に残った文字を読み捨ててから、もう一度 scanf を呼びます。

長い名前の残りが次の入力に残る

原因: %19s で最大19文字までに制限しているためです。
対処: この回では、配列サイズを超えないための制限として扱います。長い入力を受け付ける必要がある場合は、残った文字の扱いも考える必要があります。

長すぎる入力では、20文字目以降が次の入力に残ることがあります。
そのため、長い名前を入れた直後の点数入力で、思わぬ読み取り失敗が起こる場合があります。

0.1 を入力すると 0 になる

原因: %d は整数として読める先頭部分だけを読み取ります。0.1 の場合は 0 まで読み取って成功し、残りの .1 は入力に残ります。
対処: 小数や余分な文字まで厳密に扱いたい場合は、読み取った後に行末まで残りを確認する方法が必要です。

input error だけでは原因が分かりにくい

原因: 入力失敗と範囲外を同じメッセージにしているためです。
対処: 必要なら、入力失敗と範囲外で別のメッセージを表示します。

練習用コード

範囲外メッセージを分ける

点数が範囲外の場合に、専用のメッセージを表示してみます。

if (students[i].score < 0 || students[i].score > 100) {
    printf("score must be between 0 and 100\n");
    return 0;
}

人数を定数で管理する

3 を何度も書く代わりに、定数として管理できます。

#define STUDENT_COUNT 3

使う側は次のように書きます。

struct Student students[STUDENT_COUNT];
input_students(students, STUDENT_COUNT);

まとめ

今回のポイントは次のとおりです。

  • scanf は読み取りに成功した項目数を返す
  • scanf("%d", &x) != 1 で整数入力の失敗を確認できる
  • 入力値が正しい型でも、範囲チェックが必要な場合がある
  • int に収まらないほど大きな入力は、scanf の戻り値だけでは安全に扱いにくい
  • 関数の戻り値で成功と失敗を表せる
  • 入力に失敗したら、その後の計算や保存を続けない
  • 文字列入力では配列サイズを超えないように最大文字数を指定する

この回では、scanf のエラー処理を丁寧にする方法を学びました。
入力処理では、読み取りに成功したか、値が許可された範囲にあるかを確認することが重要です。小数、余分な文字、大きすぎる数まで厳密に扱う場合は、文字列として1行読み取ってから変換する方法も選択肢になります。

次回予告

次は、第20回の総合演習を複数ファイルに分割する流れを学びます。
入力、計算、保存の処理を別ファイルに分けることで、プログラムの構成をより整理できます。

復習してみよう

復習したい方は、こちらもあわせてご覧ください。

C言語の次にUnityやUnreal Engineを学ぶ選択肢

用途ごとに選ぶ C言語のおすすめ本

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

のいのアバター のい UNIX Cafe マスター

Macintosh Color Classicから始まった旅は、長いWindows時代を経て、Windows10のサポート終了をきっかけにUNIXの世界へ戻ってきました。UNIX Cafeでは、UNIX・Linux・そしてMacな世界を、むずかしい言葉を使わず、物語のように書いています。プログラミングは、アイデアをコンピューターに伝えるための言葉です。簡単な単語と文法を覚えれば、誰でもコマンドを使えます。ぜひ一度、やさしいプログラミングの世界をのぞいてみてください。

目次