C言語のきほん|分割コンパイルとリンクの仕組み

1つの大きなプログラムを、見やすい部品に分けて組み立てよう

これまでの学習では、main 関数も補助関数も、すべて1つのソースファイルにまとめて書くことが多かったと思います。
小さなプログラムならその形でも十分ですが、プログラムが少し大きくなると、だんだん見通しが悪くなってきます。

たとえば、

  • main 関数が長くなりすぎる
  • いろいろな関数が1つのファイルに集まりすぎる
  • どこに何が書いてあるのか探しにくくなる
  • 1か所だけ直したいのに全体を見直す必要が出る

といったことが起こりやすくなります。

そこで役立つのが、分割コンパイル です。
これは、プログラムを役割ごとに複数のソースファイルへ分けて書き、それぞれをコンパイルしてから、最後にまとめて1つの実行ファイルへ組み立てる方法です。

この考え方を身につけると、

  • プログラムを整理しやすい
  • 関数ごとの役割がはっきりする
  • 修正箇所を追いやすい
  • 変更したファイルだけを再コンパイルできる

といった大きな利点があります。

C言語では、少し規模のあるプログラムになると、この「分けて作って、あとでつなぐ」という考え方がとても大切になります。
ここでは、分割コンパイルとリンクの流れを、シンプルな例に置き換えながら、やさしく丁寧に整理していきましょう。

分割コンパイルとは

分割コンパイルとは、1つのプログラムを複数のソースファイルに分けて作成し、それぞれを個別にコンパイルする方法です。

たとえば、次のように役割ごとに分けられます。

  • main.c
    プログラムの入口 main 関数を書く
  • total.c
    合計を求める関数を書く
  • add.c
    2つの値を足す小さな関数を書く
  • common.h
    共通で使うマクロや関数宣言を書く

このように分けておくと、main.c は全体の流れだけ、total.c は合計処理だけ、add.c は加算処理だけ、というふうに役割がはっきりします。

リンクとは

分割コンパイルでは、各ソースファイルをいきなり実行ファイルにするのではなく、まずオブジェクトファイルという途中のファイルを作ります。

たとえば、

  • main.c → main.o
  • total.c → total.o
  • add.c → add.o

のような形です。

そして、そのあとで、それらのオブジェクトファイルをリンクして、1つの実行ファイルを作ります。

つまり、流れとしてはこうです。

手順内容
1ソースファイルを分けて書く
2各ソースファイルを個別にコンパイルする
3できたオブジェクトファイルをまとめてリンクする
41つの実行ファイルが完成する

この「最後につなぎ合わせる作業」がリンクです。

今回のサンプル構成

今回は、配列の合計を求めるプログラム を分割コンパイルする例にします。

使うファイルは次の4つです。

ファイル名役割
common.hマクロ定義と関数プロトタイプ宣言を書く
main.c配列を用意し、合計を表示する
total.c配列の合計を求める関数を書く
add.c2つの整数を足す関数を書く

この構成にすると、main.c が total_array を呼び出し、total.c がさらに add_two を呼び出す、という形になります。
複数のソースファイルが協力して1つのプログラムを作る流れを確認しやすいです。

分割コンパイルの全体の流れ

まずは全体像をつかみましょう。

① ヘッダファイルを作成する

ヘッダファイルには、複数のソースファイルで共通して使う内容を書きます。

たとえば、

  • 関数プロトタイプ宣言
  • マクロ定義
  • 構造体の宣言

などです。

今回なら、common.h に

  • DATA_COUNT
  • total_array 関数の宣言
  • add_two 関数の宣言

を書きます。

② ソースファイルを作成する

各ソースファイルには、関数の本体を分けて書きます。

  • main.c には main 関数
  • total.c には total_array 関数
  • add.c には add_two 関数

を書きます。

③ 各ソースファイルをコンパイルする

各ファイルを個別にコンパイルして、オブジェクトファイルを作ります。

④ オブジェクトファイルをリンクする

最後に、それらをまとめてリンクして実行ファイルを作ります。

図で流れをイメージしよう

分割コンパイルの流れは、図で見るとかなり整理しやすいです。

この図では、common.h を各ソースファイルが読み込み、それぞれが個別にコンパイルされてオブジェクトファイルになる流れを表しています。
そのあと、main.o、total.o、add.o がリンクされ、最終的に total_program という実行ファイルになります。

① ヘッダファイル common.h

まず、共通ヘッダファイルを作ります。

#ifndef COMMON_H
#define COMMON_H

#define DATA_COUNT 5

// 関数プロトタイプ宣言
int total_array(const int values[], int size);
int add_two(int a, int b);

#endif

このヘッダファイルの役割

このヘッダファイルには、複数のソースファイルで共通して使う内容を書いています。

内容役割
DATA_COUNT配列の要素数を統一する
total_array の宣言main.c や total.c で使えるようにする
add_two の宣言total.c や add.c で使えるようにする

このように、ヘッダファイルは「共有の約束事を書く場所」と考えると分かりやすいです。

② main.c メインプログラム

次に main.c です。

#include <stdio.h>
#include "common.h"

int main(void)
{
    int values[DATA_COUNT] = {12, 8, 25, 10, 15};

    printf("データ一覧:\n");
    for (int i = 0; i < DATA_COUNT; i++) {
        printf("%d ", values[i]);
    }
    printf("\n");

    int total = total_array(values, DATA_COUNT);

    printf("合計値: %d\n", total);

    return 0;
}

main.c の役割

main.c は、プログラム全体の入口になるファイルです。

ここでは、

  • 配列を用意する
  • 中身を表示する
  • total_array を呼び出す
  • 合計値を表示する

という流れを担当しています。

つまり、main.c は「全体の流れを管理する役割」を持っています。

③ total.c 合計処理

次に total.c です。

#include "common.h"

int total_array(const int values[], int size)
{
    int total = 0;

    for (int i = 0; i < size; i++) {
        total = add_two(total, values[i]);
    }

    return total;
}

total.c の役割

このファイルには、配列の合計を求める total_array 関数が書かれています。

ここでは、配列の各要素を順番に見ながら、add_two 関数を使って合計を更新しています。

つまり total.c は、「配列の合計を求める処理」を担当しています。

④ add.c 加算処理

最後に add.c です。

#include "common.h"

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

add.c の役割

このファイルには、2つの整数を足すだけの add_two 関数を書いています。

とても小さな関数ですが、こうして別ファイルに分けることで、「小さな部品を組み合わせてプログラムを作る」感覚がつかみやすくなります。

実行結果例

このプログラムを実行すると、たとえば次のようになります。

データ一覧:
12 8 25 10 15
合計値: 70

コンパイルの方法

分割コンパイルでは、まず各ソースファイルを個別にコンパイルします。

GCC なら、たとえば次のように書けます。

gcc -c main.c
gcc -c total.c
gcc -c add.c

これで、次のオブジェクトファイルが作られます。

  • main.o
  • total.o
  • add.o

リンクの方法

次に、それらをまとめてリンクします。

gcc main.o total.o add.o -o total_program

これで、実行ファイル total_program ができます。

Windows 環境では、見た目としては total_program.exe になることがあります。

一括でコンパイルとリンクをする方法

分割コンパイルの手順を学ぶうえでは、コンパイルとリンクを分けて実行するほうが分かりやすいです。
ただし、実際には一括で実行することもできます。

gcc main.c total.c add.c -o total_program

この1行で、

  • コンパイル
  • リンク

がまとめて行われます。

なぜ分けてコンパイルするのか

ここで、「一括でできるなら、なぜわざわざ分けてコンパイルするのか」と思うかもしれません。
理由はとても大事です。

見通しがよくなる

役割ごとにファイルを分けることで、どこに何があるか分かりやすくなります。

分け方効果
main.c全体の流れが見やすい
total.c合計処理だけを集中して見られる
add.c基本関数を独立して確認できる

変更箇所だけ再コンパイルできる

たとえば add.c だけ直したなら、本来は add.c だけ再コンパイルすれば十分です。
すべてを毎回コンパイルし直すより、効率がよくなります。

これが、ビルド時間短縮 の大きな理由です。

作業分担しやすい

複数人で開発するなら、

  • Aさんは main.c
  • Bさんは total.c
  • Cさんは add.c

のように分担しやすくなります。

ビルドとは何か

ここで関連用語として、ビルド も押さえておきましょう。

ビルドとは、コンパイルだけを指すのではなく、実行ファイルを作るまでの一連の流れ全体 を指す広い言葉です。

つまり、今回の例なら、

  • ソースファイルを読む
  • コンパイルする
  • オブジェクトファイルを作る
  • リンクする
  • 実行ファイルを作る

この流れ全体がビルドです。

#include "common.h" の意味

今回の各ソースファイルでは、共通ヘッダを次のように読み込んでいます。

#include "common.h"

これは、「自分で作成したヘッダファイル common.h を読み込む」という意味です。

標準ライブラリのようなヘッダは

#include <stdio.h>

のように山かっこで書きますが、自作ヘッダは通常ダブルクォーテーションで囲みます。

インクルードガードとは

ヘッダファイルでは、同じ内容が何度も読み込まれると困ることがあります。
そのため、普通は インクルードガード を付けます。

今回の common.h では次の部分です。

#ifndef COMMON_H
#define COMMON_H

/* ヘッダの内容 */

#endif

それぞれの意味

記述意味
#ifndef COMMON_Hまだ COMMON_H が定義されていないなら続ける
#define COMMON_HCOMMON_H を定義する
#endif条件分岐を終える

なぜ必要なのか

もし common.h が何度も読み込まれると、同じ宣言が重複してコンパイルエラーになることがあります。
インクルードガードを使えば、2回目以降は読み飛ばされるので安全です。

サンプル構成を表で整理

今回の例を整理すると、次のようになります。

ファイル主な内容役割
common.hマクロ定義、関数宣言共通ルールをまとめる
main.cmain 関数全体の流れを担当
total.ctotal_array配列の合計を求める
add.cadd_two2つの整数を足す

このように、ヘッダファイルは「宣言」、ソースファイルは「定義 本体」と分けて考えると整理しやすいです。

どんなときに分割コンパイルが役立つのか

分割コンパイルは、次のような場面で特に役立ちます。

場面役立つ理由
関数が増えてきたとき役割ごとに整理しやすい
修正が多いとき変更部分だけ再コンパイルしやすい
複数人で作業するとき分担しやすい
部品として再利用したいとき関数群を独立させやすい

小さな練習では1ファイルでも大丈夫ですが、少しずつ規模が大きくなると、この考え方のありがたさがはっきり見えてきます。

シンプルな補足図

分割コンパイルでは、「宣言はヘッダ、定義はソース」という関係も図にすると分かりやすいです。

学習のコツ

分割コンパイルを理解するときは、次の3つを意識すると整理しやすいです。

見るポイント意識したいこと
ヘッダファイル宣言を書く場所
ソースファイル処理の本体を書く場所
リンク分かれた部品を最後に1つへつなぐ作業

特に大事なのは、プログラムを分けても、最後にはちゃんと1つにつながる という感覚です。

最初は1ファイルで書いていた関数を、
「この関数は別ファイルに分けても動くかな」
と考えてみると、分割コンパイルの意味がぐっと見えやすくなります。