🇯🇵 日本語 | 🇺🇸 English
広告スペース

C言語 マルチファイル構成

1つの .c ファイルから「ヘッダ・ソース分離+Makefile」の実践構成に移行する。

なぜファイルを分けるのか

コードが1000行を超えてくると、1つの .c ファイルでは読みづらく、ビルドも遅くなります。ファイルを分割すると次のメリットがあります。

ヘッダ・ソース分離の実例

簡単な数学関数ライブラリを3ファイルに分けます。
mathutil.h(ヘッダ)
// 宣言(プロトタイプ)のみを置く
#ifndef MATHUTIL_H
#define MATHUTIL_H

int add(int a, int b);
int mul(int a, int b);
int factorial(int n);

#endif
mathutil.c(実装)
#include "mathutil.h"

int add(int a, int b) {
    return a + b;
}

int mul(int a, int b) {
    return a * b;
}

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
main.c(利用側)
#include <stdio.h>
#include "mathutil.h"

int main(void) {
    printf("2+3 = %d\n", add(2, 3));
    printf("4*5 = %d\n", mul(4, 5));
    printf("5! = %d\n", factorial(5));
    return 0;
}
役割の違い: .h は「このモジュールが外に提供するもの(宣言)」の一覧。.c は「実際にどう動くか(実装)」。ヘッダだけ見れば関数の使い方が分かるのが理想。
#include "..." と <...> の違い: ダブルクォートは 自分のプロジェクト内のヘッダ、山括弧は標準ライブラリや外部ライブラリのヘッダに使うのが慣習。

ビルドの流れと gcc オプション

gcc は実は「プリプロセス → コンパイル → アセンブル → リンク」の4段階を内部で行っています。普段は1コマンドで全部やってくれますが、段階を意識するとマルチファイル構成が理解しやすい。

3ファイルを一気にビルド

$ gcc main.c mathutil.c -o app $ ./app 2+3 = 5 4*5 = 20 5! = 120

段階を分けてビルド(差分ビルドの基礎)

$ gcc -c main.c # main.o を作る (コンパイルのみ) $ gcc -c mathutil.c # mathutil.o を作る $ gcc main.o mathutil.o -o app # リンク
次回 main.c だけ変更したら gcc -c main.c と最後の gcc main.o mathutil.o -o app の2コマンドで済む(mathutil は再コンパイル不要)。これを自動化するのが Makefile。

よく使う gcc オプション

オプション意味
-cコンパイルのみ(リンクしない)。.o ファイル生成
-o name出力ファイル名を指定
-Wall -Wextra警告を広めに出す。常に付けるべき
-gデバッグ情報付き(gdb で使う)
-O0 / -O2最適化なし / 標準的最適化
-std=c11 / -std=c17C言語規格のバージョンを指定
-I dirヘッダ検索パスを追加
-L dirライブラリ検索パスを追加
-l nameライブラリ libname.a/so をリンク(例: -lm で math)
-D MACROプリプロセッサにマクロを定義(#define MACRO 相当)

include guard と extern

include guard が必要な理由

同じヘッダが複数箇所から include されると、中身が二重に展開されて再定義エラーになります。次のマクロで1度だけ展開を保証します。
// mathutil.h の冒頭と末尾
#ifndef MATHUTIL_H
#define MATHUTIL_H

/* ヘッダの中身 */

#endif
modernな代替: gcc/clang では #pragma once 1行でも同じ効果。ただし標準ではないので、教科書では #ifndef を勧めることが多い。

グローバル変数を共有する(extern)

複数の .c から同じグローバル変数を使いたいとき、ヘッダには extern 宣言(場所を教えるだけ)、1つの .c には実体の定義を書きます。
config.h
#ifndef CONFIG_H
#define CONFIG_H

extern int g_verbose;   // 宣言のみ(実体はどこかに)

#endif
config.c
#include "config.h"

int g_verbose = 0;      // 実体の定義(唯一1箇所)
ヘッダに int g_verbose; と書くのはNG: include した全ての .c で実体が定義されてしまい、リンカエラー(multiple definition)になる。

Makefile 入門

make コマンドは Makefile を読んで「どのファイルが変更されたか」を判定し、必要な部分だけビルドします。最小の Makefile から始めましょう。

最小版

# Makefile(ファイル名は必ず大文字M)
# 注意: インデントは必ずタブ文字

app: main.o mathutil.o
	gcc main.o mathutil.o -o app

main.o: main.c mathutil.h
	gcc -c main.c

mathutil.o: mathutil.c mathutil.h
	gcc -c mathutil.c

clean:
	rm -f *.o app
$ make gcc -c main.c gcc -c mathutil.c gcc main.o mathutil.o -o app $ make # 2回目: 変更なし → 何もしない make: 'app' is up to date. $ make clean # ビルド成果物を削除

変数と暗黙ルールで短縮

CC      = gcc
CFLAGS  = -Wall -Wextra -O2 -g
OBJS    = main.o mathutil.o
TARGET  = app

$(TARGET): $(OBJS)
	$(CC) $(OBJS) -o $(TARGET)

# .c → .o の変換は暗黙ルールが適用される
# make は自動的に $(CC) $(CFLAGS) -c foo.c -o foo.o を実行

main.o: main.c mathutil.h
mathutil.o: mathutil.c mathutil.h

clean:
	rm -f $(OBJS) $(TARGET)

.PHONY: clean
.PHONY: clean というファイルが実在しないのに常に実行したい場合に宣言する。ファイルがあると「up to date」になってしまうのを防ぐ。

依存関係の自動生成(発展)

# gcc の -MMD で .d ファイル(依存関係)を生成
CFLAGS += -MMD
-include $(OBJS:.o=.d)
ヘッダの変更を自動検出して再コンパイルしてくれる。大規模プロジェクトで重要。

チャレンジ課題

課題1: 3ファイル構成を実際に作る
mathutil.h / mathutil.c / main.c を作り、Makefile でビルド。make./app で実行できるところまで。
課題2: 数学ライブラリをリンク
mathutil.c で sqrt()<math.h>)を使う関数を追加し、Makefile の CFLAGS または LDFLAGS に -lm を追加してビルド成功させよ。
課題3: デバッグビルドとリリースビルド
make debug-g -O0make release-O2 になるように Makefile に2つのターゲットを追加せよ。
課題4: 再定義エラーを実演
include guard をわざと外したヘッダを作り、2箇所から include して #include "x.h" のときに出るエラーメッセージを確認せよ。その後 guard を戻して再ビルド。