導入
1つの .c ファイルに何千行も書き続けるプロジェクトはありません。実際の開発では、「計算処理」「入出力」「データ構造」のように機能ごとにファイルを分け、必要なところだけ #include して使います。この分割のルールと道具立てを押さえておくと、初めて複数ファイルのプロジェクトを渡されたときに迷わなくなります。
説明
宣言と定義を分ける
Cのファイル分割の基本は、「宣言(こういう関数・変数がありますよ、という約束)」と「定義(実際の中身)」を分けることです。宣言は .h(ヘッダファイル)に、定義は .c(ソースファイル)に書くのが定石です。
/* calc.h ― 宣言だけを書く */
#ifndef CALC_H
#define CALC_H
int add(int a, int b); /* プロトタイプ宣言(中身はない) */
extern int callCount; /* extern宣言:実体は他のファイルにある */
#endif
/* calc.c ― 定義(実体)を書く */
#include "calc.h"
int callCount = 0; /* ここが実体。定義は1か所だけ */
int add(int a, int b) {
callCount++;
return a + b;
}
/* main.c ― calc.h をインクルードして使う */
#include <stdio.h>
#include "calc.h"
int main(void) {
printf("%d\n", add(2, 3));
printf("呼び出し回数=%d\n", callCount);
return 0;
}
#include "calc.h" のようにダブルクオートで書くのは、標準ライブラリ(<stdio.h> のように山括弧で書く)ではなく、自分のプロジェクト内のファイルを指すという合図です。main.c は add の中身を知らなくても、calc.h の宣言だけを見てコンパイルできます。
インクルードガード ― もう一度のおさらい
第9章で、同じ内容が2回 #include されると「同じものを2回定義した」というエラーになることを学びました。複数ファイルの開発では、1つのヘッダが a.c からも b.c からも、あるいは別のヘッダ経由でも #include されることが普通に起こるため、calc.h のようにヘッダの中身全体を #ifndef/#define/#endif の3点セットで囲むインクルードガードが欠かせません。多くのコンパイラでは、同じ役割を1行で済ませる #pragma once も使えます(標準規格には入っていない、いわば業界標準の拡張ですが、実務ではよく見かけます)。
#pragma once /* インクルードガードの代わりにこれ1行だけでもよい(拡張機能) */
extern ― 変数を他のファイルへ公開する
calc.h に書いた extern int callCount; は、「callCount という int 型の変数がどこかに実体としてある」という宣言です。実体(定義)は calc.c の int callCount = 0; の1行だけで、これはプロジェクト全体でちょうど1か所に書きます。ヘッダに extern を付けずに int callCount; と書いてしまうと、複数の .c からそのヘッダを読み込んだときに「同じ変数を複数回定義した」というエラーになりやすいので注意してください。
コンパイルからリンクまでの流れ
複数ファイルのプロジェクトをビルドするとき、コンパイラは各 .c ファイルを個別にオブジェクトファイル(.o)へコンパイルし、最後にリンカがそれらを1つの実行ファイルにまとめます。
flowchart LR m["main.c"] -->|"コンパイル"| mo["main.o"] c["calc.c"] -->|"コンパイル"| co["calc.o"] mo -->|"リンク"| link["リンカ"] co -->|"リンク"| link link --> app["app(実行ファイル)"]
コマンドラインでは、次のように書きます。
# 1本のコマンドで、コンパイルとリンクをまとめて行う
gcc main.c calc.c -o app
# 段階を分けて書くと、上の1行が何をしているかがわかる
gcc -c main.c -o main.o # main.c だけをコンパイル(リンクはまだしない)
gcc -c calc.c -o calc.o # calc.c だけをコンパイル
gcc main.o calc.o -o app # オブジェクトファイル同士をリンクして実行ファイルに
main.c を1行直しただけなら main.o だけを再コンパイルして calc.o はそのまま使い回せるので、大きなプロジェクトほどこの分割がビルド時間の節約にも役立ちます。main.c は add の実装(中身)を一切見ていないのに、calc.h の宣言とリンクの仕組みのおかげで正しく add を呼び出せる、という点がポイントです。
このサイトのブラウザ実行環境は単一ファイルしか扱えないため、複数ファイルへの分割そのものを実際に動かして確かめることはできません。この先、自分のPCに開発環境を用意してプロジェクトを組むときに、この章の内容を思い出してください。
まとめ
- 宣言(プロトタイプ・
extern変数)は.hに、定義(実体)は.cに書く。定義は同じものをプロジェクト全体で1か所だけに置く。 #include "自分のファイル.h"はダブルクオート、標準ライブラリは<山括弧>。- ヘッダの中身は必ずインクルードガード(
#ifndef/#define/#endif、または#pragma once)で囲む。 - ビルドは「各
.cを.oにコンパイル → リンクして実行ファイルにまとめる」の2段階(gcc a.c b.c -o appは裏でこれをまとめてやってくれる)。