C言語を基礎から順に学びたい場合は、C言語 学習ロードマップ を参照してください。
構造体と動的メモリの基礎を確認してから読むと、複数のデータを保存する設計につなげやすくなります。
C言語でデータを保存する|ファイル操作・ヘッダーファイル・複数ファイルへの分割
プログラムを終了すると、変数や配列に入っていた値は基本的に消えます。
計算結果、設定値、商品一覧、ログのように、後で使いたいデータはファイルへ保存します。
また、プログラムが大きくなると、すべての処理を1つの .c ファイルへ書くのは読みづらくなります。
C言語では、役割ごとに .c ファイルを分け、共通して使う型や関数の宣言を .h ファイルへ置きます。
この記事では、商品データを題材に、次の内容を学びます。
-
fopenでファイルを開く -
fprintfでファイルへ書き込む -
fgetsで1行ずつ読み込む -
fcloseで必ず閉じる -
"r"、"w"、"a"の使い分け -
.hファイルと.cファイルの役割 - ヘッダーファイルのインクルードガード
- 複数のソースファイルをまとめてコンパイルする
前提
この記事は、次の内容を学んだ人を対象にしています。
- 構造体、
typedef、enum - 配列と
sizeof - ポインタと
const - 関数、戻り値、
NULL
先に、次の記事を読んでください。
C言語の基本|初めてのプログラムを動かしながら、書き方と考え方を学ぶ
構造体の記事を読んでから進むと、最後の複数ファイルの例を理解しやすくなります。
C言語でデータをまとめて扱う|構造体・typedef・enum・構造体配列
動的メモリを使う場合は、メモリ確保と解放のルールも確認しておきます。
C言語の動的メモリ管理入門|malloc・calloc・realloc・free・memset
1. ファイルへ保存する理由
変数に入っている値は、プログラムが終了すると消えます。
プログラムを実行する
↓
変数へ値を入れる
↓
プログラムを終了する
↓
変数の値は消える
ファイルへ保存すると、次回の実行後もデータを残せます。
プログラムでデータを作る
↓
ファイルへ書き込む
↓
プログラムを終了する
↓
後でファイルを読み込める
ファイル操作では、次の流れを守ります。
fopen でファイルを開く
↓
開けたか確認する
↓
読み込みまたは書き込みを行う
↓
fclose でファイルを閉じる
ファイルを開いたら、最後に必ず fclose で閉じます。
2. ファイルへ文字を書き込む
最初に、ファイルへ文字を書き込む最小のプログラムを作ります。
write_file.c というファイルを作成してください。
#include <stdio.h>
int main(void)
{
// 開いたファイルを扱うためのポインタです。
FILE *file_pointer = NULL;
// sample.txt を書き込み用で開きます。
// "w" は書き込み用で開く指定です。
file_pointer = fopen("sample.txt", "w");
// ファイルを開けなかった場合は、以降の処理を行いません。
if (file_pointer == NULL)
{
printf("sample.txt を開けませんでした。\n");
return 1;
}
// fprintf は、画面ではなくファイルへ文字を書き込みます。
if (fprintf(file_pointer, "C言語でファイルへ書き込みました。\n") < 0)
{
printf("ファイルへ書き込めませんでした。\n");
fclose(file_pointer);
return 1;
}
if (fprintf(file_pointer, "2行目の文字です。\n") < 0)
{
printf("ファイルへ書き込めませんでした。\n");
fclose(file_pointer);
return 1;
}
// ファイルを閉じます。
// fclose の失敗も確認します。
if (fclose(file_pointer) == EOF)
{
printf("ファイルを閉じるときに失敗しました。\n");
return 1;
}
printf("sample.txt を作成しました。\n");
return 0;
}
コンパイルして実行します。
gcc -std=c17 -Wall -Wextra -Wpedantic write_file.c -o write_file
./write_file
実行後、同じフォルダーに sample.txt が作成されます。
中身は次のとおりです。
C言語でファイルへ書き込みました。
2行目の文字です。
fprintf の役割
fprintf は、printf と似ています。
| 関数 | 出力先 |
|---|---|
printf |
画面 |
fprintf |
指定したファイル |
printf("画面へ表示します。\n");
fprintf(file_pointer, "ファイルへ書き込みます。\n");
fprintf の最初の引数には、書き込み先のファイルを渡します。
3. ファイルから1行ずつ読み込む
次に、作成した sample.txt を読み込みます。
先に write_file.c を実行して、sample.txt を作成してください。
read_file.c というファイルを作成します。
#include <stdio.h>
int main(void)
{
// 開いたファイルを扱うためのポインタです。
FILE *file_pointer = NULL;
// 1行分の文字を入れる配列です。
// 最大255文字と、文字列の終わりを表す値1つを入れられます。
char line[256];
// sample.txt を読み込み用で開きます。
// "r" は読み込み用で開く指定です。
file_pointer = fopen("sample.txt", "r");
if (file_pointer == NULL)
{
printf("sample.txt を開けませんでした。\n");
return 1;
}
// ファイルの終わりまで、1行ずつ読み込みます。
while (fgets(line, sizeof line, file_pointer) != NULL)
{
// line には改行も含まれるため、%s の後に改行は付けません。
printf("%s", line);
}
// 読み込みエラーが起きていないか確認します。
if (ferror(file_pointer))
{
printf("ファイルの読み込み中にエラーが起きました。\n");
fclose(file_pointer);
return 1;
}
if (fclose(file_pointer) == EOF)
{
printf("ファイルを閉じるときに失敗しました。\n");
return 1;
}
return 0;
}
コンパイルして実行します。
gcc -std=c17 -Wall -Wextra -Wpedantic read_file.c -o read_file
./read_file
実行結果です。
C言語でファイルへ書き込みました。
2行目の文字です。
fgets の役割
fgets は、ファイルから1行を読み込み、char 型の配列へ入れる関数です。
fgets(line, sizeof line, file_pointer)
| 引数 | 意味 |
|---|---|
line |
読み込んだ文字を入れる配列 |
sizeof line |
読み込める最大サイズ |
file_pointer |
読み込むファイル |
1行読めた場合は、line に文字列が入り、戻り値は NULL 以外になります。
ファイルの終わりまで読んだ場合や読み込みに失敗した場合は、NULL になります。
そのため、次の形で最後まで読み込めます。
while (fgets(line, sizeof line, file_pointer) != NULL)
{
printf("%s", line);
}
4. ファイルを開く指定
最初に覚える指定は、次の3つです。
| 指定 | 意味 | 注意点 |
|---|---|---|
"r" |
読み込み用で開く | ファイルがない場合は失敗する |
"w" |
書き込み用で開く | ファイルがあれば中身をすべて上書きする |
"a" |
追記用で開く | 既存の末尾に追加する |
たとえば、ログを末尾へ追加する場合は "a" を使います。
FILE *file_pointer = fopen("application.log", "a");
"w" は既存の内容を消して書き直すため、特に注意してください。
"w" で開く
↓
既存のファイルがある
↓
中身は上書きされる
5. ファイル操作で必ず確認すること
ファイル操作では、次の3点を必ず確認してください。
fopen が NULL ではないか
↓
読み込み・書き込みが失敗していないか
↓
fclose で閉じたか
特に、fopen の結果を確認せずに fprintf や fgets を使うのは危険です。
FILE *file_pointer = fopen("sample.txt", "r");
if (file_pointer == NULL)
{
printf("ファイルを開けませんでした。\n");
return 1;
}
ファイルがない、権限がない、保存先が使えないなど、開けない理由は複数あります。
6. 1つのCファイルが大きくなる問題
小さなプログラムでは、すべてを main.c に書いても問題ありません。
しかし、処理が増えると次のようになりがちです。
main.c
├ Product 構造体
├ 商品表示
├ 商品検索
├ ファイル保存
├ ファイル読込
├ 入力処理
└ main 関数
この状態では、どこに何が書かれているのか探しにくくなります。
そこで、役割ごとにファイルを分けます。
役割ごとに分けたときの #include 関係を確認します。
矢印は、矢印元のファイルが矢印先のヘッダーファイルを #include する関係です。
product_file.h の中では、Product 型を使うために #include "product.h" も書きます。
次に、コンパイルの流れを確認します。
main.c、product.c、product_file.c の3つをまとめて指定し、product_app を作成します。
main.c
プログラム全体の流れ
product.h
Product 型と商品表示関数の宣言
product.c
商品表示関数の実装
product_file.h
ファイル保存・読込関数の宣言
product_file.c
ファイル保存・読込関数の実装
7. .h ファイルと .c ファイルの役割
| ファイル | 主な役割 |
|---|---|
.h |
型、定数、関数の宣言を共有する |
.c |
実際の処理を書く |
関数は、次の2つに分けて考えます。
// 関数宣言
void print_product(const Product *product);
// 関数定義
void print_product(const Product *product)
{
// 実際の処理
}
product.h には宣言を書き、product.c には定義を書きます。
ほかの .c ファイルは、#include "product.h" と書くことで、Product 型や関数宣言を共有できます。
#include "..." と #include <...>
| 書き方 | 主な用途 |
|---|---|
#include <stdio.h> |
C言語の標準ヘッダーファイル |
#include "product.h" |
自分で作ったヘッダーファイル |
8. インクルードガードを付ける
ヘッダーファイルは、複数の場所から読み込まれることがあります。
同じ内容が何度も読み込まれるのを防ぐため、インクルードガードを付けます。
#ifndef PRODUCT_H
#define PRODUCT_H
// ヘッダーファイルの内容
#endif
意味は次のとおりです。
PRODUCT_H がまだ定義されていない場合だけ
↓
このヘッダーファイルの内容を読み込む
最初のうちは、ヘッダーファイルを作ったら次の形をそのまま使って問題ありません。
#ifndef ファイル名に対応する名前
#define ファイル名に対応する名前
// 宣言
#endif
9. 商品一覧を保存する複数ファイルのプログラム
ここからは、商品一覧を products.txt へ保存し、保存した内容を画面へ表示するプログラムを作ります。
フォルダー内に、次の5ファイルを作成してください。
product_app/
├ main.c
├ product.h
├ product.c
├ product_file.h
└ product_file.c
product.h
#ifndef PRODUCT_H
#define PRODUCT_H
#include <stddef.h>
// 商品名に使う文字数です。
#define PRODUCT_NAME_LENGTH 32
// 商品1件を表す型です。
typedef struct
{
int id;
char name[PRODUCT_NAME_LENGTH];
int unit_price;
int stock_count;
} Product;
// 商品1件を画面へ表示する関数です。
void print_product(const Product *product);
#endif
product.c
#include <stdio.h>
#include "product.h"
// 商品1件を画面へ表示する関数です。
void print_product(const Product *product)
{
// NULL が渡された場合は何もしません。
if (product == NULL)
{
return;
}
printf("商品ID: %d\n", product->id);
printf("商品名: %s\n", product->name);
printf("単価: %d円\n", product->unit_price);
printf("在庫数: %d個\n", product->stock_count);
}
product_file.h
#ifndef PRODUCT_FILE_H
#define PRODUCT_FILE_H
#include <stddef.h>
#include "product.h"
// 商品一覧をテキストファイルへ保存します。
// 成功した場合は1、失敗した場合は0を返します。
int save_products_to_file(
const char *file_path,
const Product products[],
size_t product_count);
// テキストファイルの内容を1行ずつ画面へ表示します。
// 成功した場合は1、失敗した場合は0を返します。
int print_file_contents(const char *file_path);
#endif
product_file.c
#include <stdio.h>
#include "product_file.h"
// 商品一覧をテキストファイルへ保存します。
int save_products_to_file(
const char *file_path,
const Product products[],
size_t product_count)
{
// 書き込み用でファイルを開きます。
FILE *file_pointer = fopen(file_path, "w");
if (file_pointer == NULL)
{
return 0;
}
// 1行目に見出しを書き込みます。
if (fprintf(file_pointer, "ID,商品名,単価,在庫数\n") < 0)
{
fclose(file_pointer);
return 0;
}
// 商品を1件ずつ、カンマ区切りのテキストとして保存します。
for (size_t index = 0; index < product_count; index++)
{
if (fprintf(
file_pointer,
"%d,%s,%d,%d\n",
products[index].id,
products[index].name,
products[index].unit_price,
products[index].stock_count) < 0)
{
fclose(file_pointer);
return 0;
}
}
// ファイルを閉じ、失敗していないか確認します。
if (fclose(file_pointer) == EOF)
{
return 0;
}
return 1;
}
// テキストファイルの内容を1行ずつ画面へ表示します。
int print_file_contents(const char *file_path)
{
char line[256];
FILE *file_pointer = fopen(file_path, "r");
if (file_pointer == NULL)
{
return 0;
}
while (fgets(line, sizeof line, file_pointer) != NULL)
{
printf("%s", line);
}
// ファイル末尾ではなく、読み込みエラーで終わった場合を確認します。
if (ferror(file_pointer))
{
fclose(file_pointer);
return 0;
}
if (fclose(file_pointer) == EOF)
{
return 0;
}
return 1;
}
main.c
#include <stdio.h>
#include "product.h"
#include "product_file.h"
int main(void)
{
// 商品一覧を作成します。
Product products[] = {
{ 101, "りんご", 120, 10 },
{ 102, "みかん", 100, 8 },
{ 103, "牛乳", 210, 5 }
};
// 配列の要素数を自動で取得します。
size_t product_count = sizeof products / sizeof products[0];
printf("商品一覧\n");
for (size_t index = 0; index < product_count; index++)
{
print_product(&products[index]);
printf("\n");
}
// 商品一覧をファイルへ保存します。
if (!save_products_to_file("products.txt", products, product_count))
{
printf("商品一覧を保存できませんでした。\n");
return 1;
}
printf("products.txt を作成しました。\n\n");
printf("保存したファイルの内容\n");
// 保存したファイルの内容を表示します。
if (!print_file_contents("products.txt"))
{
printf("products.txt を読み込めませんでした。\n");
return 1;
}
return 0;
}
複数の .c ファイルをコンパイルする
複数の .c ファイルがある場合は、必要なファイルをすべて指定してコンパイルします。
gcc -std=c17 -Wall -Wextra -Wpedantic \
main.c product.c product_file.c \
-o product_app
実行します。
./product_app
実行後、products.txt が作成されます。
ID,商品名,単価,在庫数
101,りんご,120,10
102,みかん,100,8
103,牛乳,210,5
この例では、商品名にカンマが含まれないことを前提にしています。
実務でCSVを厳密に扱う場合は、カンマ、改行、ダブルクォートを含む値の扱いを別途考える必要があります。
10. 役割ごとにファイルを分ける考え方
今回のファイル構成を整理します。
| ファイル | 役割 |
|---|---|
main.c |
プログラム全体の流れを決める |
product.h |
Product 型と商品表示関数の宣言を共有する |
product.c |
商品表示の処理を書く |
product_file.h |
ファイル保存・読込関数の宣言を共有する |
product_file.c |
ファイル保存・読込の処理を書く |
ファイルを分ける目的は、ファイル数を増やすことではありません。
処理の役割を分ける
↓
読む場所を探しやすくする
↓
変更の影響を小さくする
↓
テストや修正をしやすくする
main.c が長くなってきたら、役割ごとに関数へ分け、その関数群を別ファイルへ移すことを検討してください。
11. 確認課題
課題1:追記モードでログを保存する
application.log を追記モードの "a" で開き、次の文字を末尾へ追加してください。
アプリケーションを起動しました。
答えを見る
#include <stdio.h>
int main(void)
{
FILE *file_pointer = fopen("application.log", "a");
if (file_pointer == NULL)
{
printf("application.log を開けませんでした。\n");
return 1;
}
if (fprintf(file_pointer, "アプリケーションを起動しました。\n") < 0)
{
printf("ログを書き込めませんでした。\n");
fclose(file_pointer);
return 1;
}
if (fclose(file_pointer) == EOF)
{
printf("ファイルを閉じるときに失敗しました。\n");
return 1;
}
return 0;
}
課題2:商品表示の見出しを変更する
product.c の print_product を変更し、商品名を次の形式で表示してください。
商品名: りんご(在庫: 10個)
課題3:保存するファイル名を変更する
main.c の products.txt を inventory.txt へ変更してください。
保存処理と読み込み処理の両方を変更する必要があります。
まとめ
ファイル操作では、次の順番を守ります。
fopen で開く
↓
NULL ではないか確認する
↓
読む・書く
↓
fclose で閉じる
| 関数・指定 | 役割 |
|---|---|
fopen |
ファイルを開く |
fprintf |
ファイルへ書き込む |
fgets |
ファイルから1行読み込む |
fclose |
ファイルを閉じる |
"r" |
読み込み用 |
"w" |
書き込み用。既存内容は上書き |
"a" |
追記用 |
複数ファイルへ分けるときは、次の役割分担を意識してください。
.h
= 型・定数・関数宣言を共有する
.c
= 実際の処理を書く
main.c
= プログラム全体の流れを書く
この構成を基礎にすると、構造体で表したデータを保存し、役割ごとに処理を分けたCプログラムを作れるようになります。
次に取り組むこと
ここまでで、C言語の基本文法、ポインタ、構造体、動的メモリ管理、ファイル操作、複数ファイルへの分割までを一通り学びました。
次は、練習問題や小さなプログラムを作り、値の集計、検索、入力チェック、データ保存を組み合わせて使う練習に進みます。
コードを書くときの基準は、次の記事も参照してください。