【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.c

mathops.h

プロジェクト/ファイル名: Lesson45_2/mathops.h

#ifndef MATHOPS_H
#define MATHOPS_H
void mul(int, int);
void divi(int, int);
extern int calcRes;      /* グローバルを宣言だけ */
#endif

mathops.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);
#endif

screen.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 = 4

3.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.oscreen.o など個別に生成されたオブジェクトファイルは、最後のリンカ段階で一つに束ねられ、ライブラリ (libc, libm など) が組み込まれて実行形式になります。

まとめ

  • 複数ヘッダに分割すると、大規模プロジェクトでも変更範囲を局所化できる
  • グローバル変数共有には extern + 実体は1か所 が原則
  • ヘッダガードと依存最小化でビルドを高速・安全に保とう

 自分のプロジェクトでも関心ごとごとにヘッダを整理し、今回のサンプル構成を試してみてください。保守性とコンパイル効率が大きく向上します。