第1回 分割コンパイルとは?
今回から、C/C++の分割コンパイルに関する連載を開始します。少なくとも、まともな入門書の1冊ぐらいは理解していることを前提ですので、必要なかたは入門書を復習するようにしてください。
単一のソースファイルでも書けないわけではないけれど...
C/C++の入門者ないしは初級者の場合、たったひとつのソースファイルに必要なコードをすべて詰め込んでしまいがちです。そうした傾向があるというより、それ以外のやり方を知らない、あるいは分割できることを知っていてもどうすればよいか分からないのが普通です。入門者の場合はさらに、何でもかんでも main 関数に詰め込んでしまいがちです。これも傾向というよりは、関数の作り方がよく分からないことが原因のようです。
入門書の内容程度であれば、たかだか100行程度のコードでしょうから、ソースファイルを分割しなくても十分扱えますし、main 関数で全部書いても何とかなります。しかし、ちょっと複雑なプログラムを書くようになると、すぐに数千行以上になりますし、本格的なプログラムになると、数万行ないし数十万行、あるいはそれ以上になってきます。また、ひとりだけで開発するのではなく、何人かのチームで開発する機会も多くなってきます。
こうなってくると、単一のソースファイルに何もかも詰め込んでいるといろいろ問題が出てきます。一番はっきりしているのは何人かのチームで開発する場合で、誰かがソースファイルを編集しているあいだに、他のメンバーがソースファイルを編集しようとすると、つじつまが合わなくなったり、保存のタイミング次第では、修正したはずの内容が反映されなかったりと、いろいろな問題が出始めます。適切なツールを使えばある程度は緩和しますが、それでも限界があります。
チームではなく、ひとりで開発する場合にも問題は起こります。大きなソースファイルをエディタで開く場合、エディタがかなり重くなることが予想されます。また、常にプログラム全体をコンパイルすることになるので、当然コンパイル時間もかかります。それ以上に、プログラムの見通しは悪くなりますし、ある場所で行った修正の影響範囲が広いため、バグが発生しやすくなります。
こうした問題を解消するために、ソースファイルを分割する必要が出てきます。
ソースファイルを分割する
それでは、単一のソースファイルに何でもかんでも詰め込むのではなく、いくつかのソースファイルに分割してみましょう。
int add(int x, int y)
{
return a + y;
}
int main(void)
{
int x = 2;
int y = 3;
int z;
z = add(x, y);
printf("%d + %d = %d\n", x, y, z);
return 0;
}
本来であれば、せめて数百行ぐらいあるソースファイルを例に挙げるほうが実感しやすいのかもしれませんが、大きなスペースを取りますし、書くのも大変なので、上のような簡単なサンプルプログラムを使って解説することにします。
このプログラムでは、二つの整数値の和を求める add 関数を定義して、main 関数から呼び出しています。この add 関数を別のソースファイルに分割することにします。ファイルを分割することで、add 関数の定義を見たいときは、それが定義されたソースファイルを開けば、スクロールすることなしに(この程度のプログラムなら、分割しなくてもスクロールする必要はないかもしれませんが...)すぐ見つけることができます。また、別のプログラムで add 関数を使いたいときも簡単に再利用することができます。
int add(int x, int y)
{
return x + y;
}
この add.c を使うもっとも手っ取り早い方法は次のようにします。
#include <stdio.h>
#include "add.c"
int main(void)
{
...
}
実践的なプログラムでは、このような方法はあまり使いませんが、手っ取り早さでいえばこれが一番です。#include "add.c" と書けば、その位置に add.c の内容が取り込まれます。そして、分割前と同じように、main 関数の前で add 関数が定義されることになるわけです。
この方法でも、とりあえずソースファイルを分割することはできましたので、二人で開発する場合あっても、add 関数を修正する担当者と main 関数を編集する担当者に分かれて、並列に作業することができるようになります。また、エディタで開く際も、大きなファイルを全部開かなくても、片側だけで済むようになります。しかし、コンパイル時間は変わりませんし(むしろ若干遅くなるはずです)、修正の影響範囲についても元のままです。
分割コンパイル
先ほどの #include で取り込む方法の問題点は、分割コンパイルを行うことで解消することができます。具体的には次のようにします。
int add(int x, int y)
{
return x + y;
}
#include <stdio.h>
int add(int x, int y);
int main(void)
{
int x = 2;
int y = 3;
int z;
z = add(x, y);
printf("%d + %d = %d\n", x, y, z);
return 0;
}
add.c は先ほどと同じです。main.c では、先ほどは add.c を #include で取り込みましたが、今回は add 関数の関数原型(プロトタイプ)だけを記述し、add.c は取り込んでいません。そして、main.c と add.c をそれぞれ別にコンパイルします。例えば、GCC の場合、
gcc -c main.c
gcc add.o main.o
のようにします。Borland C++ Compiler であれば。
bcc32 -c main.c
bcc32 add.obj main.obj
とすればよいでしょう。Visual C++ であれば、プロジェクトに add.c と main.c を追加すれば、あとは IDE(統合開発環境)がうまくやってくれます。
ここで、add.o とか add.obj というのは、add.c をコンパイルしてできるファイルのことで、「オブジェクトファイル」といいます。このオブジェクトファイルを、必要なだけ並べてコンパイラを呼び出せば、全体が「リンク」され、実行ファイルができます。
オブジェクトファイルを作るためには、個々のソースファイルごとにコンパイルを行えばよいので、修正されたソースファイルだけをコンパイルしてオブジェクトファイルを作り直し、それらをリンクすればよいことになります。つまり、プログラム全体ではなく、修正されたソースファイルを再コンパイルするだけでよくなるので、開発中、コンパイルにかかる時間が短くなります。
例えば、add.c は以前のままで、 main.c だけを修正した場合、
gcc -c main.c
gcc add.o main.o
のように、main.c だけを再コンパイルし、リンクすればよいわけですから、add.c のコンパイルを省略することができるわけです。


> int add(int x, int y);
>
> int main(void)
これはやってはいけないことですね。
特に「分割コンパイルをきわめる」と題している記事では。