実践編2|C言語で総合演習を複数ファイルに分割する:役割ごとにファイルを分ける

当サイトでは、コンテンツの一部に広告を掲載しています。
実践編2|C言語で総合演習を複数ファイルに分割する:役割ごとにファイルを分ける

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

目次

はじめに

実践編1では、第20回の総合演習に scanf のエラー処理を追加しました。
今回は、その総合演習を複数ファイルに分割します。

1つの .c ファイルにすべての処理を書くと、プログラムが長くなったときに読みにくくなります。
入力、表示、計算、保存の処理を別ファイルに分けることで、役割を整理できます。

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

  • 第20回の総合演習を複数ファイルに分ける
  • .h ファイルに構造体定義と関数宣言を書く
  • .c ファイルに関数の中身を書く
  • main.c には全体の流れを書く
  • 複数ファイルをまとめてコンパイルする

今回のファイル構成

今回は、次の3つのファイルに分けます。

  • main.c
  • student.c
  • student.h

それぞれの役割は次のとおりです。

  • main.c : プログラム全体の流れを書く
  • student.c : 学生情報に関する関数の中身を書く
  • student.h : 構造体定義と関数宣言を書く

このように、学生情報に関する処理を student.cstudent.h にまとめます。

ヘッダファイルを作る

student.h

#ifndef STUDENT_H
#define STUDENT_H

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

int input_students(struct Student students[], int size);
void print_students(struct Student students[], int size);
int sum_scores(struct Student students[], int size);
int save_result(struct Student students[], int size, int sum, double average);

#endif

student.h には、構造体の定義と関数の宣言を書いています。
main.c はこのヘッダファイルを読み込むことで、struct Student と各関数を使えるようになります。

関数の中身を書く

student.c

#include <stdio.h>
#include "student.h"

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;
}

student.c には、学生情報を扱う関数の中身を書いています。
#include "student.h" によって、構造体定義と関数宣言を読み込んでいます。

全体の流れを書く

main.c

#include <stdio.h>
#include "student.h"

#define STUDENT_COUNT 3

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

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

    print_students(students, STUDENT_COUNT);

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

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

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

    return 0;
}

main.c には、プログラム全体の流れを書いています。
具体的な入力処理や保存処理の中身は、student.c 側に分けています。

実行手順

1. 作業ディレクトリを作って移動する

mkdir final_split_sample
cd final_split_sample

2. 3つのファイルを作成する

同じディレクトリに、次の3つのファイルを保存します。

  • main.c
  • student.c
  • student.h

3. コンパイルする

clang main.c student.c -o final_app

4. 実行する

./final_app

実行例:

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

5. 保存されたファイルを確認する

cat result.txt

実行結果:

Alice 80
Bob 90
Carol 70
sum 240
average 80.0

コードの読み方

student.h の役割

student.h は、他のファイルから使いたい情報をまとめるファイルです。

この例では、次の情報を書いています。

  • struct Student の定義
  • input_students の宣言
  • print_students の宣言
  • sum_scores の宣言
  • save_result の宣言

main.cstudent.h を読み込むことで、これらを使えるようになります。

student.c の役割

student.c は、関数の中身を書くファイルです。
input_studentsprint_studentssum_scoressave_result の処理はここに書きます。

main.c の役割

main.c は、プログラムの開始地点です。
細かい処理の中身ではなく、どの順番で処理を行うかを書きます。

#define STUDENT_COUNT 3

#define STUDENT_COUNT 3

学生数を定数として定義しています。
3 を何度も直接書くより、意味が分かりやすくなります。

次のように使えます。

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

学生数を変更したい場合は、#define STUDENT_COUNT 33 を変えるだけで済みます。

なぜ構造体定義をヘッダに書くのか

main.c では、次のように構造体配列を宣言しています。

struct Student students[STUDENT_COUNT];

このため、main.cstruct Student の中身を知っている必要があります。
そのため、構造体定義を student.h に書き、main.c から読み込めるようにしています。

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

student.c をコンパイルに含め忘れる

次のように main.c だけをコンパイルすると、関数の中身が見つかりません。

clang main.c -o final_app

正しくは、student.c も指定します。

clang main.c student.c -o final_app

student.h をコンパイルコマンドに書いてしまう

通常、ヘッダファイルはコンパイルコマンドに直接書きません。

clang main.c student.c -o final_app

student.h は、#include "student.h" によって読み込まれます。

関数宣言と関数定義が一致していない

student.h の宣言と student.c の定義は一致している必要があります。

宣言:

int sum_scores(struct Student students[], int size);

定義:

int sum_scores(struct Student students[], int size) {
    ...
}

戻り値の型、関数名、引数の型を一致させます。

ヘッダファイルにインクルードガードを書き忘れる

ヘッダファイルには、同じ内容が重複して読み込まれないようにインクルードガードを書きます。

#ifndef STUDENT_H
#define STUDENT_H

...

#endif

よくあるエラー

undefined symbols for architecture …

原因: student.c をコンパイルに含めていません。
対処: clang main.c student.c -o final_app のように指定します。

fatal error: ‘student.h’ file not found

原因: student.h が見つかりません。
対処: main.cstudent.cstudent.h が同じディレクトリにあるか確認します。

conflicting types for ‘sum_scores’

原因: student.h の関数宣言と student.c の関数定義が一致していません。
対処: 戻り値の型、関数名、引数の型をそろえます。

練習用コード

最大点を求める関数を追加する

student.h に宣言を追加します。

int max_score(struct Student students[], int size);

student.c に定義を追加します。

int max_score(struct Student students[], int size) {
    int i;
    int max = students[0].score;

    for (i = 1; i < size; i++) {
        if (students[i].score > max) {
            max = students[i].score;
        }
    }

    return max;
}

main.c で呼び出します。

printf("max: %d\n", max_score(students, STUDENT_COUNT));

ファイル保存の関数だけ別ファイルにする

さらに分けるなら、保存処理を file_utils.cfile_utils.h に分けることもできます。
ただし、最初から細かく分けすぎると管理が難しくなるため、この回では student.c にまとめています。

まとめ

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

  • 複数ファイルに分けると、役割ごとにコードを管理しやすくなる
  • .h ファイルには、構造体定義や関数宣言を書く
  • .c ファイルには、関数の中身を書く
  • main.c には、プログラム全体の流れを書く
  • 自分で作ったヘッダファイルは #include "student.h" のように読み込む
  • 複数の .c ファイルを使う場合は、コンパイル時にすべて指定する
  • 関数宣言と関数定義は一致させる

この回では、第20回の総合演習を複数ファイルに分割しました。
main.cstudent.cstudent.h に分けることで、全体の流れ、関数の中身、関数の宣言を整理できました。

次回予告

次は、人数を動的メモリ確保で可変にする方法を学びます。
固定の3人ではなく、実行時に入力された人数に合わせて配列を確保するように改良します。

あわせて読みたい
実践編3|C言語で人数を動的メモリ確保で可変にする:入力された人数分だけ構造体配列を確保する はじめてのC言語 | 第23回 はじめに 実践編2では、第20回の総合演習を main.c、student.c、student.h に分割しました。今回は、そのプログラムをさらに改良して、学生...

さらに学びたいあなたへ

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

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

この記事を書いた人

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

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

Created by UNIX Cafe

目次