複数ファイルプログラミング

複数ファイルプログラミングはチーム共同作業のようなものです。宣言は契約(ヘッダファイル)、実装は作業(ソースファイル)、externは部署間の参照、Makefileはプロジェクトのスケジュールです。

なぜ複数ファイルが必要なのか

小さなプログラムなら単一の .c ファイルで十分ですが、実際のプロジェクトは大規模で、機能ごとに複数のファイルに分割する必要があります:

ヘッダファイルの書き方

ヘッダファイル(.h)には宣言を含め、ソースファイル(.c)には定義を含めます。これが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

ソースファイルの内容

C
#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;
}

メインプログラム

C
#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宣言)を含め、定義(関数本体、変数の初期化)は含めないでください。定義が重複するとリンカエラーになります。

externによるファイル間参照

ある .c ファイルで定義されたグローバル変数は、他のファイルから extern を使ってアクセスできます。

定義(ある.cファイル内)

C
int g_count = 0;

宣言(ヘッダまたは別の.cファイル内)

C
extern int g_count;

extern はコンパイラに「この変数は別の場所で定義されている。これは宣言のみである」と伝え、新たな領域は割り当てられません。

プロジェクト構成:

TEXT
project/
├── counter.h
├── counter.c
└── main.c
▶ 試してみよう

counter.h:

C
#ifndef COUNTER_H
#define COUNTER_H

extern int g_count;
void increment(void);
int get_count(void);

#endif

counter.c:

C
#include "counter.h"

int g_count = 0;

void increment(void) {
    g_count++;
}

int get_count(void) {
    return g_count;
}

main.c:

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;
}
TEXT
count = 3
g_count = 3
⚠️ 注意: extern 宣言に初期化子を含めてはいけません。extern int g_count = 0; は一部のコンパイラで定義として扱われ、二重定義エラーの原因になります。

複数ファイルのコンパイルコマンド

個別にコンパイルしてからリンク

BASH
gcc -c counter.c -o counter.o
gcc -c main.c -o main.o
gcc counter.o main.o -o myapp

-c オプションはリンクせずにコンパイルし、.o オブジェクトファイルを生成します。すべての .o ファイルをリンクして最終的な実行ファイルを作ります。

一括コンパイル

BASH
gcc counter.c main.c -o myapp

簡単ですが、どれか1つのファイルを変更してもすべて再コンパイルする必要があり、大規模プロジェクトには不向きです。

💡 ヒント: 個別コンパイルの利点:main.c を変更した場合、gcc -c main.c だけを行いリンクすればよく、counter.c を再コンパイルする必要がありません。

Makefileの基本

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
⚠️ 注意: MakefileのインデントにはTab文字を使う必要があり、スペースは使えません。これが初心者が最もよく犯す間違いです。

使い方:

BASH
make
make clean

Makefileの規則の形式:

MAKEFILE
target: dependencies
	command

二重インクルードの防止

ヘッダファイルは複数のソースファイルからインクルードされる可能性があり、同じソースファイルから間接的に複数回インクルードされることもあります。保護がないと、宣言の重複がエラーを引き起こします。

方法1:インクルードガード

C
#ifndef MYHEADER_H
#define MYHEADER_H

void my_func(void);

#endif

最初のインクルード時、MYHEADER_H は未定義なので内容が処理され、マクロが定義されます。2回目以降のインクルードでは #ifndef が偽となり、内容全体がスキップされます。

方法2:#pragma once

C
#pragma once

void my_func(void);

簡潔でほとんどのコンパイラがサポートしていますが、標準ではないため、インクルードガードより移植性は理論上劣ります。

💡 ヒント: どちらの方法も実務で使われています。#pragma once の方が簡潔で、インクルードガードの方が標準的です。どちらかを選んで統一してください。

ヘッダファイルの構成原則

❓ よくある質問

Q 変数の定義をヘッダファイルに置けますか?
A いいえ。ヘッダが複数の.cファイルにインクルードされると二重定義になります。変数の定義は.cファイルに置き、.hファイルではextern宣言を使ってください。
Q externとヘッダの直接インクルードの違いは何ですか?
A #includeはヘッダファイルの内容全体を挿入します。externは変数や関数が別の場所で定義されていることを明示的に宣言します。ヘッダには通常extern宣言が含まれており、両者は併用されます。
Q Makefileのインデントは本当にTabでなければなりませんか?
A はい、Makeの厳格な要件です。スペースを使うとエラーになります。エディタでMakefileにはTabインデントを使うように設定してください。
Q #pragma onceとインクルードガードはどちらを選ぶべきですか?
A どちらかを選んでプロジェクト内で統一してください。#pragma onceはシンプルですが非標準です。インクルードガードは標準ですがやや冗長です。大規模なオープンソースプロジェクトではインクルードガードがよく使われています。

📖 まとめ

📝 練習問題

  1. 3ファイルのプロジェクト string_utils.hstring_utils.cmain.c を作成してください。文字列反転関数と文字カウント関数を実装し、main関数で呼び出してテストしてください
  2. 練習1用のMakefileを作成してください。make でコンパイル、make clean でクリーンアップできるようにしてください
  3. ヘッダファイルで意図的に二重インクルード防止を省略し、main.cで同じヘッダを2回インクルードしてコンパイルエラーを観察してください
100%