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

当サイトでは、コンテンツの一部に広告を掲載しています。
実践編1|C言語でscanf のエラー処理を丁寧にする:入力失敗を確認して安全に処理する

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

目次

はじめに

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

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

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

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

scanf の戻り値

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

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

int score;

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

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

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

ソースコード

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

整数以外を入力した例:

score: abc
input error

コードの読み方

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

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

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

1 でない場合は、読み取りに失敗したと判断できます。

return 1

return 1;

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

点数の範囲を確認する

整数として読み取れても、値が適切とは限りません。
点数として扱うなら、たとえば 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

入力用の関数を作る

入力処理を関数に分けると、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);

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

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

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

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

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

よくあるエラー

整数以外を入力すると処理が止まる

原因: %d で整数を読み取ろうとして失敗しています。
対処: 戻り値を確認し、失敗した場合はエラーメッセージを表示します。

長い名前を入力すると途中までしか保存されない

原因: %19s で最大19文字までに制限しているためです。
対処: この回では、配列サイズを超えないための制限として扱います。

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 で整数入力の失敗を確認できる
  • 入力値が正しい型でも、範囲チェックが必要な場合がある
  • 関数の戻り値で成功と失敗を表せる
  • 入力に失敗したら、その後の計算や保存を続けない
  • 文字列入力では配列サイズを超えないように最大文字数を指定する

この回では、scanf のエラー処理を丁寧にする方法を学びました。
入力処理では、読み取りに成功したか、値が許可された範囲にあるかを確認することが重要です。

次回予告

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

あわせて読みたい
実践編2|C言語で総合演習を複数ファイルに分割する:役割ごとにファイルを分ける はじめてのC言語 | 第22回 はじめに 実践編1では、第20回の総合演習に scanf のエラー処理を追加しました。今回は、その総合演習を複数ファイルに分割します。 1つの ...

さらに学びたいあなたへ

用途ごとに選ぶ Linux のおすすめ本

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

この記事を書いた人

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

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

Created by UNIX Cafe

目次