複数ファイルプログラミング
複数ファイルプログラミングはチーム共同作業のようなものです。宣言は契約(ヘッダファイル)、実装は作業(ソースファイル)、externは部署間の参照、Makefileはプロジェクトのスケジュールです。
なぜ複数ファイルが必要なのか
小さなプログラムなら単一の .c ファイルで十分ですが、実際のプロジェクトは大規模で、機能ごとに複数のファイルに分割する必要があります:
- 責任の所在が明確で論理がすっきりする
- コンパイルが速くなる。1つのファイルを変更してもそのファイルだけを再コンパイルすればよい
- コードの再利用。共通モジュールを複数プロジェクトで共有できる
- チーム共同作業。異なる人が異なるモジュールを担当できる
ヘッダファイルの書き方
ヘッダファイル(.h)には宣言を含め、ソースファイル(.c)には定義を含めます。これがC言語における最も基本的なモジュール化のルールです。
ヘッダファイルの内容
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int subtract(int a, int b);
double average(int *arr, int n);
#endif
ソースファイルの内容
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
double average(int *arr, int n) {
if (n <= 0) return 0.0;
int sum = 0;
for (int i = 0; i < n; i++) sum += arr[i];
return (double)sum / n;
}
メインプログラム
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("3 + 5 = %d\n", add(3, 5));
printf("10 - 4 = %d\n", subtract(10, 4));
int scores[] = {80, 90, 75, 88};
printf("Average: %.1f\n", average(scores, 4));
return 0;
}
externによるファイル間参照
ある .c ファイルで定義されたグローバル変数は、他のファイルから extern を使ってアクセスできます。
定義(ある.cファイル内)
int g_count = 0;
宣言(ヘッダまたは別の.cファイル内)
extern int g_count;
extern はコンパイラに「この変数は別の場所で定義されている。これは宣言のみである」と伝え、新たな領域は割り当てられません。
例
プロジェクト構成:
project/
├── counter.h
├── counter.c
└── main.c
counter.h:
#ifndef COUNTER_H
#define COUNTER_H
extern int g_count;
void increment(void);
int get_count(void);
#endif
counter.c:
#include "counter.h"
int g_count = 0;
void increment(void) {
g_count++;
}
int get_count(void) {
return g_count;
}
main.c:
#include <stdio.h>
#include "counter.h"
int main(void) {
increment();
increment();
increment();
printf("count = %d\n", get_count());
printf("g_count = %d\n", g_count);
return 0;
}
count = 3
g_count = 3
extern 宣言に初期化子を含めてはいけません。extern int g_count = 0; は一部のコンパイラで定義として扱われ、二重定義エラーの原因になります。
複数ファイルのコンパイルコマンド
個別にコンパイルしてからリンク
gcc -c counter.c -o counter.o
gcc -c main.c -o main.o
gcc counter.o main.o -o myapp
-c オプションはリンクせずにコンパイルし、.o オブジェクトファイルを生成します。すべての .o ファイルをリンクして最終的な実行ファイルを作ります。
一括コンパイル
gcc counter.c main.c -o myapp
簡単ですが、どれか1つのファイルを変更してもすべて再コンパイルする必要があり、大規模プロジェクトには不向きです。
main.c を変更した場合、gcc -c main.c だけを行いリンクすればよく、counter.c を再コンパイルする必要がありません。
Makefileの基本
Makefileはコンパイル規則を自動化し、変更されたファイルだけを再コンパイルします。
CC = gcc
CFLAGS = -Wall -g
myapp: main.o counter.o
$(CC) $(CFLAGS) main.o counter.o -o myapp
main.o: main.c counter.h
$(CC) $(CFLAGS) -c main.c -o main.o
counter.o: counter.c counter.h
$(CC) $(CFLAGS) -c counter.c -o counter.o
clean:
rm -f *.o myapp
使い方:
make
make clean
Makefileの規則の形式:
target: dependencies
command
- ターゲット:生成するファイル
- 依存関係:ターゲットが依存するファイル。依存関係のいずれかがターゲットより新しい場合、コマンドが再実行されます
- コマンド:依存関係からターゲットを生成する方法
二重インクルードの防止
ヘッダファイルは複数のソースファイルからインクルードされる可能性があり、同じソースファイルから間接的に複数回インクルードされることもあります。保護がないと、宣言の重複がエラーを引き起こします。
方法1:インクルードガード
#ifndef MYHEADER_H
#define MYHEADER_H
void my_func(void);
#endif
最初のインクルード時、MYHEADER_H は未定義なので内容が処理され、マクロが定義されます。2回目以降のインクルードでは #ifndef が偽となり、内容全体がスキップされます。
方法2:#pragma once
#pragma once
void my_func(void);
簡潔でほとんどのコンパイラがサポートしていますが、標準ではないため、インクルードガードより移植性は理論上劣ります。
#pragma once の方が簡潔で、インクルードガードの方が標準的です。どちらかを選んで統一してください。
ヘッダファイルの構成原則
- 各
.cファイルには同名の.hファイルを対応させるべきです .hファイルには宣言のみを含め、定義は含めないでください.cファイルの1行目には自身の.hをインクルードしてください(宣言と定義の一致を確認するため)- ヘッダには二重インクルード防止を必ず含めてください
externグローバル変数宣言は.hに、定義は特定の.cファイルに配置してください
❓ よくある質問
📖 まとめ
- ヘッダファイル(.h)に宣言、ソースファイル(.c)に定義という分離がモジュール化の基盤です
externはファイル間のグローバル変数や関数を宣言し、コンパイラに「別の場所で定義済み」と伝えます- 複数ファイルのコンパイルでは「-cで個別コンパイル後にリンク」を使い、変更されたファイルだけを再コンパイルしてください
- Makefileはコンパイルの依存関係を自動化し、長いコマンドを手入力する手間を省きます
- インクルードガードか
#pragma onceで二重インクルードを防止してください
📝 練習問題
- 3ファイルのプロジェクト
string_utils.h、string_utils.c、main.cを作成してください。文字列反転関数と文字カウント関数を実装し、main関数で呼び出してテストしてください - 練習1用のMakefileを作成してください。
makeでコンパイル、make cleanでクリーンアップできるようにしてください - ヘッダファイルで意図的に二重インクルード防止を省略し、main.cで同じヘッダを2回インクルードしてコンパイルエラーを観察してください



