
【6日でできるC言語入門】複数のヘッダーファイルへの分割
ソースコードが成長すると、ファイルを「ヘッダ」と「実装」に切り分けるだけでは管理が追いつかなくなります。特にチーム開発では、関心ごとごとに複数のヘッダーファイルへさらに分割し、依存関係を明示しておくことが重要です。
ここでは「複数ヘッダへの分割」をテーマに、グローバル変数の共有方法を含めた実践的なテクニックを紹介します。最後まで読めば、モジュール化されたプロジェクトを安全にビルド・保守できる設計指針が身につきます。

1.なぜ複数ヘッダに分けるのか
1.1. 単一ヘッダの限界
| 課題 | 詳細 |
|---|---|
| 変更波及が大きい | 1 行の修正で全ソースが再コンパイルされる。 |
| ネームスペース衝突 | 同名関数・マクロが膨大に増える。 |
| レビュー困難 | 定義量が多く可読性が低下。 |
1.2. 関心ごとに切り分ける戦略
| モジュール | 役割 | 典型的なペア |
|---|---|---|
| 計算ロジック | 算術・アルゴリズム | mathops.h / mathops.c |
| I/O 表示 | 画面出力・ログ | screen.h / screen.c |
| 設定管理 | 定数・設定値 | config.h / (実装不要) |
1.3. ヘッダと実装をペアにするメリット
- インターフェースと実装の分離 ― ヘッダに公開 API だけを置き、実装詳細を.c側に隠蔽
- 再利用性の向上 ― 必要なヘッダだけを
#includeすれば依存を最小化 - ビルド高速化 ― 変更が局所化され、インクリメンタルビルドが効きやすい
2.グローバル変数を共有する方法
2.1. グローバル変数の可視範囲
デフォルトでは 定義した翻訳単位(.cファイル)内のみ に実体が生成されます。他ファイルからアクセスするには宣言が必要です。
2.2. extern 修飾子
extern は「実体は外部にある」とリンカへ伝え、重複定義エラーを防ぎます。
/* どこか1ファイルで実体を定義 */
int calcRes = 0;
/* 他ファイルから参照だけするとき */
extern int calcRes;2.3. ヘッダで宣言する慣習
グローバル変数を複数ファイルで共有するなら、extern int calcRes; を専用ヘッダに置き、全ファイルで同じ宣言を使い回すと整合性が保てます。
3.サンプルで学ぶファイル分割
3.1. 分割前のコード(no_split.c)
プロジェクト/ファイル名: Lesson45_1/no_split.c
#include <stdio.h>
int calcRes = 0; /* グローバル変数 */
void mul(int, int); /* 掛け算 */
void divi(int, int); /* 割り算(整数) */
void printRes(void); /* 結果表示 */
int main(void){
int x = 8, y = 2;
printf("%d × %d = ", x, y);
mul(x, y);
printRes();
printf("%d ÷ %d = ", x, y);
divi(x, y);
printRes();
return 0;
}
void mul(int a, int b){ calcRes = a * b; }
void divi(int a, int b){ calcRes = a / b; }
void printRes(void){ printf("%d\n", calcRes); }実行結果
8 × 2 = 16
8 ÷ 2 = 4変更点が増えると再ビルドが大変です。次節で 5 ファイルに分割します。
3.2. 分割後の構成
Lesson45_2/
├── main.c
├── mathops.h
├── mathops.c
├── screen.h
└── screen.cmathops.h
プロジェクト/ファイル名: Lesson45_2/mathops.h
#ifndef MATHOPS_H
#define MATHOPS_H
void mul(int, int);
void divi(int, int);
extern int calcRes; /* グローバルを宣言だけ */
#endifmathops.c
プロジェクト/ファイル名: Lesson45_2/mathops.c
#include "mathops.h"
int calcRes = 0; /* 実体はここに1つだけ */
void mul(int a, int b){ calcRes = a * b; }
void divi(int a, int b){ calcRes = a / b; }screen.h
プロジェクト/ファイル名: Lesson45_2/screen.h
#ifndef SCREEN_H
#define SCREEN_H
void printRes(void);
#endifscreen.c
プロジェクト/ファイル名: Lesson45_2/screen.c
#include <stdio.h>
#include "mathops.h" /* calcRes を参照するために含める */
#include "screen.h"
void printRes(void){
printf("%d\n", calcRes);
}main.c
プロジェクト/ファイル名: Lesson45_2/main.c
#include <stdio.h>
#include "mathops.h"
#include "screen.h"
int main(void){
int x = 8, y = 2;
printf("%d × %d = ", x, y);
mul(x, y);
printRes();
printf("%d ÷ %d = ", x, y);
divi(x, y);
printRes();
return 0;
}3.3. 実行方法
1.「ビルド」 メニューから「ソリューションのビルド」を選択します。

2.ビルドが成功すると、「main.c」「mathops.c」「screen.c」の両方がコンパイルされ、正しくリンクされます。
3.「デバッグ」 メニューから「デバッグなしで開始」を選択します。

4.結果ウィンドウに、次のような出力が表示されます。
8 × 2 = 16
8 ÷ 2 = 43.4. 学びのポイント
| テーマ | 実装箇所 | 解説 |
|---|---|---|
| グローバル変数の一元化 | mathops.c | 実体は1ファイルだけで定義 |
| extern 宣言 | mathops.h | 参照側はヘッダ経由で共有 |
| API と実装分離 | .h vs .c | 呼び出し側は詳細を知らずに利用可能 |
4.コンパイラが行う三段階処理
- プリプロセッサ
#include,#defineなどを展開し、単一の翻訳単位を生成。 - コンパイル
翻訳単位を機械語命令へ変換し、オブジェクトファイル(.o/.obj)を生成。 - リンカ
複数のオブジェクトファイルとライブラリを結合し、実行ファイルを作成。externで宣言したシンボルをここで解決する。
5.ベストプラクティス
5.1. ヘッダガード
#ifndef MODULE_H
#define MODULE_H
/* 宣言 */
#endif多重インクルードを防ぎ、予期せぬ再定義を回避します。
5.2. 最小限の #include
ヘッダ内では前方宣言を使い、依存を減らすことでビルド時間を短縮。
5.3. 再コンパイルを減らすコツ
- 実装変更だけなら
.cのみ更新 - 公開インターフェース(.h)を安易に書き換えない
6.Cコンパイラの仕組み
C プログラムが最終的な実行ファイルになるまでには、プリプロセッサ → コンパイラ → アセンブラ → リンカ という 4 段階を順に通過します。各段階の役割をまとめると以下のとおりです。
| 段階 | 主な処理 | 出力ファイル | 代表オプション例 (GCC) |
|---|---|---|---|
| プリプロセッサ | #include 展開、マクロ置換、条件付きコンパイル | 一時テキスト | -E |
| コンパイラ | 構文解析、最適化、中間表現生成 | アセンブリ (.s) | -S |
| アセンブラ | アセンブリを機械語へ変換 | オブジェクト (.o) | -c |
| リンカ | 複数オブジェクトとライブラリを結合し外部シンボルを解決 | 実行ファイル (a.out / .exe) | (省略可) |
ポイント
extern宣言はリンカが同名シンボルを照合するための“契約”- インクリメンタルビルドでは、修正した翻訳単位だけを再コンパイルし、既存の
.oを再利用して高速化
6.1. C コンパイラの仕組み

この図は、複数ヘッダ・複数ソースに分割した今回のサンプル構成でも同様に適用されます。mathops.o や screen.o など個別に生成されたオブジェクトファイルは、最後のリンカ段階で一つに束ねられ、ライブラリ (libc, libm など) が組み込まれて実行形式になります。
まとめ
- 複数ヘッダに分割すると、大規模プロジェクトでも変更範囲を局所化できる
- グローバル変数共有には extern + 実体は1か所 が原則
- ヘッダガードと依存最小化でビルドを高速・安全に保とう
自分のプロジェクトでも関心ごとごとにヘッダを整理し、今回のサンプル構成を試してみてください。保守性とコンパイル効率が大きく向上します。
