C言語を基礎から順に学びたい場合は、C言語 学習ロードマップ を参照してください。
プログラムを終了すると、変数や配列の値は基本的に消えます。
商品一覧、設定値、ログのように後で使うデータは、ファイルへ保存します。
また、機能が増えたプログラムを1つの .c ファイルへ書き続けると、修正する場所を探しにくくなります。
この記事では、商品一覧をファイルへ保存する例を使い、次をまとめて学びます。
-
fopen、fprintf、fgets、fclose -
.cと.hの役割 - 複数の
.cファイルをGCCでコンパイルする方法 -
staticが関数・変数・ローカル変数に付いたときの動き -
externを付けたとき、付けないときの違い - グローバル変数を共有するときの正しい形
- グローバル変数を使いすぎない判断基準
グローバル変数は便利ですが、どこからでも変更できるため、原因調査を難しくします。
この記事では static と extern を理解するために使います。新しい処理では、まず関数の引数・戻り値・構造体で渡せないかを検討してください。
1. ファイル操作の基本
ファイル操作は、次の順番で行います。
| 関数・指定 | 役割 |
|---|---|
fopen |
ファイルを開く |
fprintf |
ファイルへ書き込む |
fgets |
ファイルから1行読み込む |
fclose |
ファイルを閉じる |
"r" |
読み込み用。ファイルがない場合は失敗 |
"w" |
書き込み用。既存の内容を上書き |
"a" |
追記用。既存ファイルの末尾へ追加 |
最小の書き込み例
#include <stdio.h>
int main(void)
{
FILE *file_pointer = fopen("sample.txt", "w");
if (file_pointer == NULL)
{
printf("sample.txt を開けませんでした。\n");
return 1;
}
if (fprintf(file_pointer, "C言語で保存しました。\n") < 0)
{
printf("書き込みに失敗しました。\n");
fclose(file_pointer);
return 1;
}
if (fclose(file_pointer) == EOF)
{
printf("ファイルを閉じるときに失敗しました。\n");
return 1;
}
return 0;
}
最小の読み込み例
#include <stdio.h>
int main(void)
{
char line[256];
FILE *file_pointer = fopen("sample.txt", "r");
if (file_pointer == NULL)
{
printf("sample.txt を開けませんでした。\n");
return 1;
}
while (fgets(line, sizeof line, file_pointer) != NULL)
{
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;
}
2. .c と .h の役割
C言語では、処理の中身と、他のファイルから使うための宣言を分けます。
| ファイル | 役割 |
|---|---|
.c |
関数の定義、実際の処理を書く |
.h |
型、定数、関数宣言を共有する |
main.c |
プログラム全体の流れを書く |
stock_app/
├ main.c
├ stock.h
├ stock.c
├ file_store.h
├ file_store.c
├ app_state.h
└ app_state.c
.c ファイルを #include しません。
#include "stock.c"
この書き方は、同じ関数が複数回定義される原因になります。
共有するのは .h ファイルであり、.c ファイルはコンパイルコマンドへ列挙します。
3. ヘッダーファイルにはインクルードガードを付ける
#ifndef STOCK_H
#define STOCK_H
// 宣言を書く
#endif
1回目に stock.h を読み込むと、STOCK_H が定義されます。
2回目以降は STOCK_H がすでに定義されているため、中身を読み飛ばします。
同じ型や関数宣言を複数回取り込む事故を防ぐため、ヘッダーファイルには必ず付けます。
4. static は「見える範囲」または「値が残る期間」を変える
static は、書く場所で意味が変わります。
| 書く場所 | 主な動き |
|---|---|
| 関数の前 | その .c ファイル内だけで使える関数になる |
| ファイル直下の変数 | その .c ファイル内だけで使えるグローバル変数になる |
| 関数内のローカル変数 | 関数を抜けても値が残る |
4.1 static 関数
static int normalize_stock_count(int stock_count)
{
if (stock_count < 0)
{
return 0;
}
return stock_count;
}
この関数は、定義した .c ファイルの中だけで使えます。
stock.c 内
normalize_stock_count を呼べる
main.c、file_store.c
normalize_stock_count を呼べない
static を付けない関数は、外部リンケージを持ちます。
別の .c ファイルから使うなら、ヘッダーファイルに関数宣言を書けます。
// stock.h
int normalize_stock_count(int stock_count);
ただし、他のファイルから使わせる必要がない補助関数まで公開すると、プロジェクト全体で名前がぶつかりやすくなります。
その .c の内部処理だけに使う関数は static にするのが基本です。
4.2 static を付けないとどうなるか
module_a.c と module_b.c に、同じ名前の外部関数を作るとします。
// module_a.c
int calculate_total(int value)
{
return value + 1;
}
// module_b.c
int calculate_total(int value)
{
return value + 2;
}
両方をコンパイルすると、同じ外部関数が2つあるため、リンク時に次のようなエラーになります。
multiple definition of 'calculate_total'
一方、両方を static 関数にすると、各 .c の内部だけの別関数として扱われるため、名前はぶつかりません。
static int calculate_total(int value)
{
return value + 1;
}
ただし、static を付けた関数は他の .c ファイルから呼び出せません。
4.3 static ローカル変数
int get_next_sequence(void)
{
static int sequence = 1;
return sequence++;
}
呼び出すたびに、値が残ります。
printf("%d\n", get_next_sequence());
printf("%d\n", get_next_sequence());
printf("%d\n", get_next_sequence());
実行結果です。
1
2
3
通常のローカル変数なら、関数を呼ぶたびに初期化されます。
int get_next_sequence(void)
{
int sequence = 1;
return sequence++;
}
この場合、毎回 1 が返ります。
static ローカル変数は状態を持つため、テストしにくくなります。
単純な連番や内部キャッシュなど、値を残す理由が明確な場面だけで使います。
5. extern は「実体は別の .c にある」と知らせる
extern は、変数の実体を作りません。
extern int g_saved_product_count;
これは、次の意味です。
g_saved_product_countというint型の変数は、別の場所で1回だけ定義されている。
実体は、1つの .c ファイルだけに書きます。
int g_saved_product_count = 0;
正しい配置
// app_state.h
#ifndef APP_STATE_H
#define APP_STATE_H
extern int g_saved_product_count;
#endif
// app_state.c
#include "app_state.h"
int g_saved_product_count = 0;
// file_store.c
#include "app_state.h"
void increment_saved_count(void)
{
g_saved_product_count++;
}
app_state.h を読み込んだ .c ファイルは、同じ1つの変数を参照します。
app_state.c
int g_saved_product_count = 0;
↑ 実体は1つ
main.c / file_store.c
extern int g_saved_product_count;
↑ 同じ実体を使う
extern を付けないとどうなるか
パターン1: 宣言がないまま使う
void increment_saved_count(void)
{
g_saved_product_count++;
}
この .c から変数の宣言が見えていなければ、コンパイルエラーになります。
error: 'g_saved_product_count' undeclared
パターン2: ヘッダーファイルに実体を書いてしまう
// app_state.h
int g_saved_product_count = 0;
このヘッダーファイルを main.c と file_store.c の両方で読み込むと、変数の実体が複数作られます。
multiple definition of 'g_saved_product_count'
ヘッダーファイルには extern による宣言だけを書き、実体は .c に1回だけ書きます。
パターン3: 複数の .c に static で書く
// main.c
static int g_saved_product_count = 0;
// file_store.c
static int g_saved_product_count = 0;
これはエラーになりません。
ただし、別々の変数が2個作られます。
main.c の g_saved_product_count
file_store.c の g_saved_product_count
片方を増やしても、もう片方の値は変わりません。
static は「共有を防ぐ」、extern は「1つの実体を共有する」と覚えると整理しやすくなります。
グローバル変数は static を付けなくても生存期間が長い
ファイル直下に書いた変数は、static がなくてもプログラム開始から終了まで存在します。
int g_saved_product_count = 0;
static int s_saved_product_count = 0;
違いは、主に他の .c から参照できるかどうかです。
| 書き方 | プログラム終了まで値が残る | 他の .c から参照 |
|---|---|---|
int g_count = 0; |
残る |
extern を使えばできる |
static int s_count = 0; |
残る | できない |
6. 完成例: 商品一覧を保存する
stock.h
#ifndef STOCK_H
#define STOCK_H
#define PRODUCT_NAME_LENGTH 32
typedef struct
{
int id;
char name[PRODUCT_NAME_LENGTH];
int unit_price;
int stock_count;
} Product;
void print_product(const Product *product);
#endif
stock.c
#include <stdio.h>
#include "stock.h"
static int normalize_stock_count(int stock_count)
{
if (stock_count < 0)
{
return 0;
}
return stock_count;
}
void print_product(const Product *product)
{
if (product == NULL)
{
return;
}
printf("商品ID: %d\n", product->id);
printf("商品名: %s\n", product->name);
printf("単価: %d円\n", product->unit_price);
printf("在庫数: %d個\n", normalize_stock_count(product->stock_count));
}
app_state.h
#ifndef APP_STATE_H
#define APP_STATE_H
extern int g_saved_product_count;
#endif
app_state.c
#include "app_state.h"
int g_saved_product_count = 0;
file_store.h
#ifndef FILE_STORE_H
#define FILE_STORE_H
#include <stddef.h>
#include "stock.h"
int save_products_to_file(
const char *file_path,
const Product products[],
size_t product_count);
#endif
file_store.c
#include <stdio.h>
#include "app_state.h"
#include "file_store.h"
static int write_csv_header(FILE *file_pointer)
{
return fprintf(file_pointer, "ID,商品名,単価,在庫数\n") >= 0;
}
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;
}
if (!write_csv_header(file_pointer))
{
fclose(file_pointer);
return 0;
}
g_saved_product_count = 0;
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;
}
g_saved_product_count++;
}
if (fclose(file_pointer) == EOF)
{
return 0;
}
return 1;
}
main.c
#include <stdio.h>
#include "app_state.h"
#include "file_store.h"
#include "stock.h"
int main(void)
{
Product products[] = {
{ 101, "りんご", 120, 10 },
{ 102, "みかん", 100, 8 },
{ 103, "牛乳", 210, 5 }
};
size_t product_count = sizeof products / sizeof products[0];
for (size_t index = 0; index < product_count; index++)
{
print_product(&products[index]);
printf("\n");
}
if (!save_products_to_file("products.csv", products, product_count))
{
printf("products.csv を保存できませんでした。\n");
return 1;
}
printf("%d件を products.csv へ保存しました。\n", g_saved_product_count);
return 0;
}
コンパイルと実行
gcc -std=c17 -Wall -Wextra -Wpedantic \
main.c stock.c file_store.c app_state.c \
-o stock_app
./stock_app
実行後、products.csv が作成されます。
ID,商品名,単価,在庫数
101,りんご,120,10
102,みかん,100,8
103,牛乳,210,5
7. グローバル変数を使う判断基準
| 状況 | 第一候補 |
|---|---|
| 処理へ値を渡す | 関数の引数 |
| 処理結果を返す | 戻り値、出力用の構造体 |
| 複数の値をまとめて渡す | 構造体 |
その .c 内部だけで状態を持つ |
static 変数 |
| アプリ全体で本当に1つだけの状態 |
extern を使うグローバル変数 |
extern を使うグローバル変数は、次の場合に限定すると安全です。
- アプリ全体の設定
- 初期化済みかどうかの状態
- 明確に1つだけである必要がある共有状態
- 組み込み環境のハードウェアレジスタなど、設計上共有が避けられないもの
8. 理解確認
課題1: static 関数
stock.c の normalize_stock_count から static を外し、stock.h に関数宣言を追加してください。
その後、main.c から呼び出してみてください。
次に、static を戻し、main.c から呼び出す設計が不要か考えてください。
課題2: extern の確認
g_saved_product_count の実体を app_state.h へ移動し、main.c と file_store.c の両方で読み込んでください。
どのようなリンクエラーが出るか確認した後、元の正しい形へ戻してください。
課題3: 追記モード
save_products_to_file を変更し、既存の内容を消さずに追記する関数 append_product_to_file を作ってください。
ファイルを開く指定は "a" を使います。
まとめ
- ファイル操作は、開く、確認する、読む・書く、閉じるの順に進める
-
.hには共有する型・関数宣言、.cには処理の実装を書く -
.cを#includeせず、コンパイルコマンドへ列挙する -
static関数は、その.c内だけで使える -
staticローカル変数は、関数を抜けても値が残る -
externは実体を作らず、別の.cにある変数を参照する宣言 - グローバル変数の実体は、必ず1つの
.cにだけ書く - 複数の
.cに同名のstatic変数を書くと、共有されず別々の変数になる