分割コンパイルについて

関数を多く含んだ規模の大きなプログラムを作成しようとする場合,(一つの/いくつかのグループの)関数を単位としてプログラムを分割し,複数のファイルに分けて整理することができます.

コンパイルする際は,まずそれぞれのファイルごとにコンパイルを行なってオブジェクトファイルを生成し,最後にすべてのオブジェクトファイルをリンクして実行ファイルを生成します.もちろん,すでに完成した関数からなるファイルは,あらかじめオブジェクトファイルとして用意しておくことができます.このようにすれば,実行ファイルを生成する際に未完成のプログラムのみをコンパイルすればよいので,コンパイルに要する時間を節約することができます.

分割コンパイル プログラムを分割する際に考慮するべき問題
make コマンド  UNIXが備えるコマンド "make" を用いた分割したプログラムのコンパイル


注意:サンプルファイルはすべて、windowsファイル(DOSファイル)にしてありますのでUNIXで使用する際は、改行コード変換・漢字コード変換をしてください.

分割コンパイル

ソースプログラムを複数のファイルに分割してコンパイルする操作を分割コンパイルと呼びます.また,コンパイルは個々のファイルごとに行なわれるので,これらのファイルをコンパイル単位と呼びます.以下では,分割コンパイルにおいて考慮するべき点として,関数プロトタイプ外部変数について述べます.

[関数プロトタイプ ]

A.分割コンパイルと関数プロトタイプ

プログラムを分割する際に注意するべき第1の点は,関数プロトタイプによる関数の宣言を必ず行うようにしてください。コンパイル単位ごとにその中で引用する関数のプロトタイプを宣言することが重要です。詳細な説明

例として、プログラム sample.c を考えます.
このプログラムをmain() のみを含むファイル main.c とその他の関数からなるファイル func.c に分割します.

これらのファイル(コンパイル単位)をコンパイルしてリンクするには次のようにします.

> cc -c main.c
> cc -c func.c
> cc -o sample2 main.o func.o

上記第1行と第2行のコマンドラインでは,それぞれオブジェクトファイル main.o と func.o が生成されます.最後のコマンドラインでこれらのオブジェクトファイルがリンクされて実行ファイル "sample2" が生成されます.

(注意)もちろん > cc -o sample2 main.c func.c としてソースプログラムから直接実行ファイルを生成することもできますし,func.o がすでにあるならば > cc -o sample2 main.c func.o とすることもできます.後者の場合は,func.c のコンパイルを省略することができるので,分割コンパイルの利点が生かされます.

B.ヘッダファイルによる関数プロトタイプ宣言

例えば数学関数 sin() や cos() などをプログラム中で引用した場合,これらの関数プロトタイプをいちいち宣言する必要はありません.ヘッダファイル "math.h" をインクルードしておけば,この中にすべての数学関数のプロトタイプが宣言されているからです.分割コンパイルを行なう際にも,この方法を用いればプログラム中での関数プロトタイプ宣言を省略することができます.

すなわち,あるプログラムが含む(main() を除く)関数のプロトタイプをすべて含んだヘッダファイルをあらかじめ用意しておき,分割後の各コンパイル単位の先頭の部分でこのヘッダファイルをインクルードするわけです.

この方法を用いて sample.c からヘッダファイル prot.h を作成し,このヘッダファイルをインクルードする2個のコンパイル単位に sample.c を分割した結果を main2.cfunc2.c に示します.これらのコンパイル単位から実行ファイルを生成する方法は,先ほどの例と同様に,

> cc -c main2.c
> cc -c func2.c
> cc -o sample3 main2.o func2.o

となります.

[外部変数と extern文 ]

プログラムを分割する際に注意するべき第2の点は,外部変数のあつかいです.もしコンパイラが未定義の変数を発見したならば,コンパイラはその変数が外部変数であり,どこか他のコンパイル単位で定義されているとは考えてくれません.そうではなく,ただちにコンパイルエラーになります.したがって,もしあるプログラムの初めに外部変数が定義されている場合,そのプログラムを分割する際には,分割されたコンパイル単位ごとに外部変数の宣言文の先頭に extern を付加し,その変数が他のコンパイル単位において宣言されていることをコンパイラに指示します.

ここでは, 俗に言うグローバル変数(プログラム単位のどこででも参照できる変数)のあつかいについて説明します.

main2.c, func2.c を例として説明します.このプログラムの場合, グローバル変数 global_val は,main2.c の先頭の部分で:

int global_val;

と定義され, func2.c では:

extern int global_val;

となっていることに注意してください.これは,global_val がどこか他の場所で定義されていることを表わします.

このように,外部変数の宣言があるプログラムを分割する場合は,分割後のひとつのコンパイル単位ではその外部変数の宣言を行ない,他のコンパイル単位では extern文を用いて外部変数の宣言を行なうことに注意してください.


make コマンド

UNIXが備えるコマンドのひとつに make があります.make は,分割された複数のコンパイル単位から目的とする実行ファイルを効率良く生成するコマンドです.実行ファイルを生成する手順はテキストファイル "Makefile" に記述します.以下では make と Makefile の関係,およびMakefile を記述する規則について順に説明します.

[ make と Makefile ]

実行ファイル生成の手順

分割コンパイルを効率良く行なうには,目的とする実行ファイルを生成する手順を記述したファイル,およびその手順にしたがい,場合によってはその手順の一部を省略しながら効率良く実行ファイルを生成するコマンドがあればよいことがわかります.

そのためののファイルが Makefile であり,Makefile の記述をもとにして実行ファイルを生成するコマンドが make です.make は,Makefile の記述にしたがいながら,各ファイルが作成された日付と時刻(以後,単に日付とします)をもとにして,不要と判断した処理を省略してゆきます.

例えば, オブジェクトファイル fun2.o が存在しないか,あるいは存在しても作成された日付がソースプログラム func2.c より古ければこの操作が実行されます.しかしそうでないならその実行は省略されます.

[ Makefile の規則 ]

A.基本的な規則

Makefile は,基本的にはターゲット/依存関係行とコマンド行の組を必要な数だけ並べて構成します.並べる順番は自由です.例えば,先に挙げた main2.c と func2.c から sample3 を生成する手順は:

> cc -c main2.c
> cc -c func2.c
> cc -o sample3 main2.o func2.o

でした.これを Makefile に記述すると,

# main2.c と func2.c から sample3 を生成する Makefile

sample3 : main2.o func2.o
   cc -o sample3 main2.o func2.o

main.o : main2.c prot.h
   cc -c main2.c

func.o : func2.c prot.h
   cc -c func2.c

clean :
   rm main2.o func2.o sample3

となります.

第1行はコメントです.make は,先頭が "#" の行はコメントとみなして無視します.第2行以下が Makefile の本体です.

第3行はターゲット,""(コロン),依存ファイルからなっています.ターゲットとは生成したい実行ファイルのことです(この場合は q_sort).依存ファイルとは,ターゲットの生成に必要なファイルです.依存ファイルが複数あるならば空白で区切って順に並べます.この第2行をターゲット/依存関係行と呼びます.

第4行はターゲットを生成する方法を,具体的なコマンドラインによって記述します.この第4行をコマンド行と呼びます.コマンド行は,行の頭からタブによってインデント(字下げ)されている必要があります.

コマンド行が実行されるかどうかは,ターゲットと依存ファイルが作成された日付の前後関係によって決まります.もし依存ファイルのどれかひとつでもターゲットより新しい日付のものがあれば,コマンド行は実行されます.(例えば main.c がエディットされ,しかもまだコンパイルされていなければ,この状態になります.)そうでなければ,make はコンパイルの必要がないと判断し,コマンド行は実行されません.

この Makefile は,

> make

とすることにより,ターゲット:sample3 が生成されます.

第1行のコメントは他の場所にあってもかまいません.第2行以下も,ターゲット/依存関係行とコマンド行の組が保たれていれば,並べる順番を変えることができます.

make は,起動されるとまず Makefile の最初に現われたターゲット(この場合は sample3)の生成を目標として,その生成に必要な依存ファイル(main.o と func.o)を用意します.依存ファイルがさらにターゲットとして他の行に記述されていれば,そのターゲットに対する依存ファイルを用意します.それぞれの操作において,ターゲットが存在しなければ(あるいはターゲットの日付が依存ファイルより前ならば)コマンド行を実行し,そうでなければコマンド行の実行を省略します.

もし最初のターゲット以外のターゲットを生成したいならば,そのターゲットをオペランドとして make を起動します.例えば,sample3 ではなく main.o のみを生成したいならば:

> make main.o

とします.

つまり,ターゲット/依存関係行とコマンド行の組がどこにあっても make の実行結果は同じです.

なお,最後に置かれたターゲット:clean をオペランドとして make を起動すると,対応するコマンド行が実行され,make によって生成されたファイルが消去されてディレクトリが最初の状態にもどります.このような特別な機能を持つターゲットのことを疑似ターゲットと呼びます.

B.マクロの使用

C言語においては,プログラムの最初にマクロを定義することにより,プログラムを読みやすく,また修正を容易にすることができました.同じことは Makefile でも可能です.以下に,Makefile におけるマクロの使用法を述べます.

ユーザ定義マクロ
例えば,ターゲットを作成する処理系に応じてコンパイラを cc または gcc に使い分けたいとします.このときは Makefile の最初の部分で:
  CC = cc
としておきます.この結果,マクロ "CC" が定義されました.以後は Makefile の中の cc を $(CC) に置き換えておけば,コンパイラを gcc にしたいときに:
  CC = gcc
とマクロを書き換えるだけで対処できます.一般に,Makefile の最初の部分でマクロ定義をしておけば,そのマクロ名を括弧でくくって(マクロ名が1文字なら括弧は不要)$を付ければ Makefile の中で自由に使用できます.

そのほか, 一般的に使われるマクロ名には次のようなものがあります.

CFLAGS コンパイラに対するオプション 例 -g (gdb 用) -pgrog など
LFLAGS リンカに対するオプション   例 -lm  
CC C言語のコンパイラのコマンド名
MAKE makeコマンドのコマンド名
内部マクロ
内部マクロは,make があらかじめ定義しているマクロです.知っていると便利な内部マクロに "$@" と "$*" があります(マクロ名はそれぞれ@と*ですが,いずれも1文字なので括弧は不要です).$@はターゲット名を表わし,$*はターゲット名から拡張子を除いた部分を表わします.例えば,先に挙げた:
  sample3 : main.o func.o
     cc -o q_sort main.o func.o
のコマンド行は
     cc -o $@ main.o func.o
に,また
  main.o : main.c
     cc -c main.c
のコマンド行は
     cc -c $*.c
にそれぞれ置き換えることができます(依存ファイルの記述までこれらのマクロで置き換えると make の動作がおかしくなるようです).

では,[make と Makefile]の項のはじめに挙げた sample3 の生成手順:

> cc -c main2.c
> cc -c func2.c
> cc -o sample3 main2.o func2.o

を,これらの規則を使って Makefile に表わしてみます(実際にコピーして Makefile として使用できるファイルをこちらに用意しました).

# main2.c と func2.c から sample3 を生成する Makefile
# コンパイラは gcc を使用

CC = gcc

sample3 : main2.o func2.o
   $(CC) -o $@ main2.o func2.o

main2.o : main2.c prot.h
   $(CC) -c $*.c

func2.o : func2.c prot.h
   $(CC) -c $*.c

clean :
   rm prot.h main2.o func2.o q_sort prt

生成手順と比べることにより,Makefile の記述方法をよく理解しておいてください.


さらに高度な分割プログラミングについて

外部変数(グローバル変数)の数が多いプログラムを分割した場合は,それらの定義を一つのファイルにまとめておき,コンパイル単位ごとにそのファイルをインクルードした方がプログラムの分割が容易になります.しかし, 外部変数(グローバル変数)の定義からなるファイルと,extern文を用いた外部変数の宣言からなるファイルの2種類が必要になります. 前者のファイルはひとつのコンパイル単位からインクルードし,後者のファイルは他のすべてのコンパイル単位からインクルードします.

この2種類のファイルの相違点は何でしょうか?一方にはexternがあり, 他方にはないだけです. これならば, 一方のファイルをコピーし, externの部分だけを書き換えれば, 他方のファイルができてしまいます. きわめて機械的な単純な変更作業です. この作業に, 「マクロ」を使用できます.

externになったり, ブランクになったりするのは, グローバル変数をそこで確保したいか, 他で確保(extern)させたいかなので, その判定をする基準となるものが必要になります. そのために, ここでは、GLOBAL_VALUE_DEFINE というマクロ名をフラグとして使うことにします. このマクロ名が #define で定義されているときは, ここで変数を宣言し, 未定義のときはextern宣言します. このためには,

	#ifdef	GLOBAL_VALUE_DEFINE
	#define	GLOBAL
	#else
	#define	GLOBAL	extern
	#endif

のように, #ifdefでGLOBALというマクロ名を空白かexternに置換します. これで,

	GLOBAL	short	SV_HEAD;

と書かれていた個所のGLOBALの部分は, 希望通りに置換されます. GLOBAL_VALUE_DEFINE は, 今回の例 sample.c ではmain2.c で定義すれば完成です.
初期値を設定するグローバル変数の例も含めて分割ファイル, ヘッダファイルを以下においてあります.
Makefile def.h prot.h main2.c func2.c
興味がある人は見てください.

「マクロを使用したグローバル変数の処理」の 元ネタURL