C言語で成績管理システムを作る【第2回】【キーボード入力対応】

目次

この記事について

「C言語で成績管理システムを作る」シリーズの第2回です。仕様決めの経緯は全体像の記事にまとめているので、前回(構造体配列で管理・集計する版)は第1回の記事をご覧ください。

成績管理システムはC言語の課題としてもよく出される定番のお題です。
もし課題で調べに来た方は、コードをそのまま写すのではなく、「なぜこの設計にしたか」の部分を読んでから自分で書いてみることをおすすめします。

このステップでは、以下の範囲を実装します。

  • 前提:第1回で作成したソースコードに追加する形で作成
  • scanfでキーボードから人数・名前・点数を入力
  • 入力値の検証に対応

scanf はC言語学習の序盤に登場する割に、「変な値を入力すると無限ループになる」「Visual Studioで警告が出る」など、初心者がつまずくポイントが集中している関数です。この記事では、実際に動くコードと一緒にその対策も解説します。

実行結果とソースコード

先に、完成したプログラムと実行した結果を載せておきます。

実行結果

赤枠内がキーボードで入力した値、それ以外はプロブラム実行時に表示される値です。

範囲外の値や数字以外を入力した場合は、こうなります。

不正な入力を弾いて、正しい値が入るまで再入力を求めるようになっています。

ソースコード

/*
 * 成績管理システム(パターン2: キーボード入力版)
 * Visual Studio 2026 で動作確認想定
 * ファイルは「UTF-8 シグネチャ(BOM)付き」で保存すること
 */
#define _CRT_SECURE_NO_WARNINGS  /* VSでのscanf使用時のC4996警告を抑止 */
#include <stdio.h>

#define MAX_NAME     32   /* 名前の最大文字数(終端の\0を含む) */
#define MAX_STUDENTS 10   /* 受け付ける最大人数 */

 /* 学生1人分のデータ */
struct Student {
    char name[MAX_NAME];
    int  score;           /* 0〜100点 */
};

/* --- 関数プロトタイプ宣言 --- */
int    input_students(struct Student s[], int max);
void   print_all(const struct Student s[], int n);
double calc_average(const struct Student s[], int n);
int    find_max_index(const struct Student s[], int n);
int    find_min_index(const struct Student s[], int n);
void   clear_input(void);

int main(void)
{
    struct Student students[MAX_STUDENTS];
    int n;
    int max_i, min_i;

    printf("===== 成績管理システム =====\n");

    /* キーボードから入力(パターン2) */
    n = input_students(students, MAX_STUDENTS);

    printf("\n");
    print_all(students, n);

    max_i = find_max_index(students, n);
    min_i = find_min_index(students, n);

    printf("--- 統計情報 ---\n");
    printf("平均点: %.1f 点\n", calc_average(students, n));
    printf("最高点: %d 点 (%s)\n", students[max_i].score, students[max_i].name);
    printf("最低点: %d 点 (%s)\n", students[min_i].score, students[min_i].name);

    return 0;
}

/* 入力バッファに残った文字を読み捨てる */
void clear_input(void)
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF) {
        /* 何もしない(読み捨てるだけ) */
    }
}

/*
 * 学生データをキーボードから入力する
 * 戻り値: 実際に入力された人数
 */
int input_students(struct Student s[], int max)
{
    int n = 0;
    int i;

    /* 人数の入力(1〜maxの範囲になるまで繰り返す) */
    for (;;) {
        printf("学生の人数を入力してください (1~%d人): ", max);
        if (scanf("%d", &n) == 1 && n >= 1 && n <= max) {
            clear_input();
            break;
        }
        printf("入力が正しくありません。\n");
        clear_input();
    }

    /* 各学生の名前と点数の入力 */
    for (i = 0; i < n; i++) {
        printf("\n%d人目の名前: ", i + 1);
        scanf("%31s", s[i].name);   /* 31文字まで読み込み(バッファあふれ防止) */
        clear_input();

        /* 点数の入力(0〜100の範囲になるまで繰り返す) */
        for (;;) {
            printf("%d人目の点数 (0~100): ", i + 1);
            if (scanf("%d", &s[i].score) == 1
                && s[i].score >= 0 && s[i].score <= 100) {
                clear_input();
                break;
            }
            printf("入力が正しくありません。\n");
            clear_input();
        }
    }

    return n;
}

/* 全員分の成績を一覧表示する */
void print_all(const struct Student s[], int n)
{
    int i;

    printf("--- 成績一覧 ---\n");
    printf("番号  名前            点数\n");
    for (i = 0; i < n; i++) {
        printf("%2d    %-15s %4d\n", i + 1, s[i].name, s[i].score);
    }
    printf("\n");
}

/* 平均点を計算して返す(人数0のときは0.0を返す) */
double calc_average(const struct Student s[], int n)
{
    int    i;
    double sum = 0.0;

    if (n <= 0) {
        return 0.0;
    }
    for (i = 0; i < n; i++) {
        sum += s[i].score;
    }
    return sum / n;
}

/* 最高点の学生の添字を返す(同点なら先に見つかった方) */
int find_max_index(const struct Student s[], int n)
{
    int i;
    int max_i = 0;

    for (i = 1; i < n; i++) {
        if (s[i].score > s[max_i].score) {
            max_i = i;
        }
    }
    return max_i;
}

/* 最低点の学生の添字を返す(同点なら先に見つかった方) */
int find_min_index(const struct Student s[], int n)
{
    int i;
    int min_i = 0;

    for (i = 1; i < n; i++) {
        if (s[i].score < s[min_i].score) {
            min_i = i;
        }
    }
    return min_i;
}

今回使う環境

  • Windows 11
  • Visual Studio 2026(コードを書いて実行するツール)
  • Claude(AI)

VS2026のインストール方法は以下の記事で解説しています。

Visual Studio 2026 でのビルド/実行手順

以前Claudeにソースコード生成を依頼した際、Visual Studioでの実行手順を教えてくれたので、その手順に従って進めます。

新しいプロジェクトの作成

.cファイルを作成

「デバッグなしで開始(Ctrl + F5)」で実行

補足:デバッグコンソールで日本語が文字化けする場合

以下のように、デバッグコンソールで日本語が文字化けする場合は「UTF-8 シグネチャ(署名)付き」で保存してください。

保存形式が原因の文字化けについてはこちらの記事で解説しています。

コードの解説

前回からの変更点・追加点を中心に解説します。構造体や集計処理(平均・最高・最低)の解説は第1回の記事をご覧ください。

scanfとC4996警告(対応マクロ:_CRT_SECURE_NO_WARNINGS)

Visual Studioで scanf を使うと、「C4996: ‘scanf’: This function or variable may be unsafe.」というエラーが出てビルドできません。これはMicrosoftが、より安全な scanf_s の使用を推奨しているためです。

今回は、コードの1行目に以下のマクロを定義することでこの警告を抑止し、標準の scanf を使う方針にしました。

#define _CRT_SECURE_NO_WARNINGS

scanf_s はVisual Studio以外の環境では使えないことが多く、学習用のコードとしては移植性の高い標準関数を使いたかったためです。なお、このマクロは必ず #include <stdio.h> より前に書く必要があります(後に書くと効きません)。

_CRT_SECURE_NO_WARNINGS の注意点

_CRT_SECURE_NO_WARNINGS のマクロを使用する際は、以下の点に注意が必要です。

  • あくまでコンパイラの警告を黙らせるだけ
  • scanf 自体の危険性(想定より長い文字列が入力される、など)がなくなるわけではない
  • scanf だけでなく strcpysprintf など、Microsoftが「安全でない」と判定している関数すべての警告をそのファイル単位でまとめて無効にしてしまう
  • Visual Studio独自のマクロのため、Linux/macOSなど他の環境でコンパイルする場合は意味を持たない

今回のコードで %31s のように自分で文字数を制限しているのは、警告が教えてくれるはずの注意点を自分で肩代わりしている、とも言えます。

入力値の検証(scanfの戻り値チェック)

scanf は「正しく読み取れた項目数」を戻り値として返します。これを利用して、数字以外の入力を検出できます。

if (scanf("%d", &n) == 1 && n >= 1 && n <= max) {
    /* 正しい入力 */
}

%d (10進数の整数)に対して abc のような文字を入力すると読み取りに失敗し、戻り値が0になります。「戻り値が1(=1項目読めた)かつ範囲内」のときだけ受理する、という書き方です。

入力バッファのクリア(clear_input関数)

scanf でつまずく人が最も多いのがここだと思います。

%d (10進数の整数)の読み取りに失敗したとき、入力した文字(abc など)は入力バッファに残ったままになります。そのまま再度 scanf を呼ぶと、残った abc をまた読もうとして失敗し、無限ループに陥ります。

そこで、失敗したら改行までの文字をすべて読み捨てる処理を関数にしました。

void clear_input(void)
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF) {
        /* 何もしない(読み捨てるだけ) */
    }
}

「不正入力のたびに 入力が正しくありません。 と表示して再入力を待つ」という当たり前に見える動作は、実はこの読み捨て処理があって初めて成立します。

読み込み文字数の制限(%31s)

名前の入力は scanf("%31s", s[i].name) としています。

%s に数字を付けると「最大でその文字数まで」しか読み込まなくなり、用意した配列(32バイト)からあふれるのを防げます。

第1回で「名前は最大31文字(配列32バイト − 終端文字1バイト)」という仕様にしましたが、入力対応した今回、この31という数字が実際にコード上で意味を持つようになりました。

数字を付けずに %s だけで受けるとバッファオーバーフローの危険があるので、必ず付けるようにしましょう。

人数を戻り値で返す設計

input_students 関数は実際に入力された人数を戻り値で返し、main はその人数を使って表示・集計します。

第1回で各関数に「配列と人数」を渡す設計にしておいたおかげで、人数が実行時に決まるようになっても、表示・集計側の関数は一切変更せずにそのまま使えました。関数分割の恩恵を実感できたポイントです。

気づいたこと

第1回との差分から見えること

実際に第1回のコードと 差分(diff)を取ってみると、各関数の変更の有無は以下の通りでした。

  • 第1回コードと差分あり
    • 人数の扱い
    • 入力周りの新規関数2つ(clear_inputinput_students)
  • 第1回コードと差分なし:
    • print_all関数
    • calc_average関数
    • find_max_index関数
    • find_min_index関数

これは偶然ではなく、第1回の時点で各関数に「データの配列」と「人数」を引数として渡す設計にしていたことの効果です。

データの取得方法(ソースコード内に直接書くか、キーボードから入力するか)が変わっても、「配列と人数さえ渡せば動く」関数側は影響を受けない、という関数分割のメリットを、今回の改修で実感する形になりました。

まとめ

  • scanf の戻り値をチェックすることで、数字以外の不正な入力を検出できる
  • 入力失敗時は入力バッファのクリア(読み捨て)が必須。これを怠ると無限ループになる
  • %31s のように読み込み文字数を制限して、配列あふれを防ぐ
  • Visual StudioのC4996警告は _CRT_SECURE_NO_WARNINGS で抑止できる(stdio.hより前に書く)
  • 第1回との差分は入力周りの新規関数2つに閉じており、集計・表示系の関数は無修正で済んだ。関数分割(データを引数で渡す設計)の効果を実感できた
  • 第3回では、switch 文を使ったメニュー画面への発展を予定

最後まで読んでいただき、ありがとうございました!

興味がある方は、シリーズの他の記事もあわせてどうぞ。

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

コメント

コメントする

目次