
はじめてのC言語 | 第22回
はじめに
実践編1では、第20回の総合演習に scanf のエラー処理を追加しました。
今回は、その総合演習を複数ファイルに分割します。
1つの .c ファイルにすべての処理を書くと、プログラムが長くなったときに読みにくくなります。
入力、表示、計算、保存の処理を別ファイルに分けることで、役割を整理できます。
この回の目的は次の5点です。
- 第20回の総合演習を複数ファイルに分ける
.hファイルに構造体定義と関数宣言を書く.cファイルに関数の中身を書くmain.cには全体の流れを書く- 複数ファイルをまとめてコンパイルする
今回のファイル構成
今回は、次の3つのファイルに分けます。
main.cstudent.cstudent.h
それぞれの役割は次のとおりです。
main.c: プログラム全体の流れを書くstudent.c: 学生情報に関する関数の中身を書くstudent.h: 構造体定義と関数宣言を書く
このように、学生情報に関する処理を student.c と student.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);
#endifstudent.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_sample2. 3つのファイルを作成する
同じディレクトリに、次の3つのファイルを保存します。
main.cstudent.cstudent.h
3. コンパイルする
clang main.c student.c -o final_app4. 実行する
./final_app実行例:
name: Alice
score: 80
name: Bob
score: 90
name: Carol
score: 70
Alice 80
Bob 90
Carol 70
sum: 240
average: 80.05. 保存されたファイルを確認する
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.c は student.h を読み込むことで、これらを使えるようになります。
student.c の役割
student.c は、関数の中身を書くファイルです。input_students、print_students、sum_scores、save_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 3 の 3 を変えるだけで済みます。
なぜ構造体定義をヘッダに書くのか
main.c では、次のように構造体配列を宣言しています。
struct Student students[STUDENT_COUNT];このため、main.c は struct Student の中身を知っている必要があります。
そのため、構造体定義を student.h に書き、main.c から読み込めるようにしています。
初心者がつまずきやすい点
student.c をコンパイルに含め忘れる
次のように main.c だけをコンパイルすると、関数の中身が見つかりません。
clang main.c -o final_app正しくは、student.c も指定します。
clang main.c student.c -o final_appstudent.h をコンパイルコマンドに書いてしまう
通常、ヘッダファイルはコンパイルコマンドに直接書きません。
clang main.c student.c -o final_appstudent.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.c、student.c、student.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.c と file_utils.h に分けることもできます。
ただし、最初から細かく分けすぎると管理が難しくなるため、この回では student.c にまとめています。
まとめ
今回のポイントは次のとおりです。
- 複数ファイルに分けると、役割ごとにコードを管理しやすくなる
.hファイルには、構造体定義や関数宣言を書く.cファイルには、関数の中身を書くmain.cには、プログラム全体の流れを書く- 自分で作ったヘッダファイルは
#include "student.h"のように読み込む - 複数の
.cファイルを使う場合は、コンパイル時にすべて指定する - 関数宣言と関数定義は一致させる
この回では、第20回の総合演習を複数ファイルに分割しました。main.c、student.c、student.h に分けることで、全体の流れ、関数の中身、関数の宣言を整理できました。
次回予告
次は、人数を動的メモリ確保で可変にする方法を学びます。
固定の3人ではなく、実行時に入力された人数に合わせて配列を確保するように改良します。


