分割コンパイルとmakeは大規模なプログラムを開発する際、なくてはならない存在になってきます。 特に分割コンパイルは、1箇所を修正してコンパイルし直すのに10分も待たされる、なんてことがなくなります。 コンパイラの消費メモリも少なくて済むので、その方法を覚えておくのに損はないでしょう。分割コンパイル
ソースが巨大になってくると、コンパイル時間が長くなったり、メモリを多く消費したり、 プログラムの見通しが悪くなったり(バグを探すのにたいへん)と、色々困ることが増えてきます。
しかし、C言語ではソースを関数単位で分割し、別々にコンパイルすることが出来ます。 ソースが小さければ上の問題点は解消されます。ソースの管理が大変になる場合もありますが、 makeと組み合わせて使えば簡略化出来ます。
数々のメリットがあるこの分割コンパイルの方法について説明しましょう。
ソースを分割する際に注意する点としては、「関数単位で分割する」ことです。 当然のことですが、関数の途中から区切って二つのファイルに分けることは出来ません。
細かくわけすぎるのも、無駄にコンパイル回数を増やすだけになるので、適度にしましょう。 似た系統の関数を一つのファイルにするなど、ある程度まとめるのもいいでしょう。
さて、変数や関数には有効範囲というものがあります。 その変数、関数がどこまで参照出来るかを表すものです。
変数の場合、プログラム全体から参照出来る、ソースの中だけで有効、関数の中だけで有効、 と大まかには三段階にわかれています。例を挙げてみます。
=============== main.c ==================== short a; short b=256; static short c; main() { short d; .... } init() { short d; .... } =========================================== =============== sub1.c ==================== short a; extern short b; static short c; sub1() { short d; .... } ===========================================
どちらのソースにもa,b,c,dの4つの変数が定義されています。それぞれの変数について見てみましょう。 変数 a は両方のソースからアクセス出来る変数です。変数 b も同様にアクセス出来ます。 ただしmain.cで初期値が設定されているので、ほかのソースから参照するためにはextern宣言をしなければなりません。
変数 c はどちらのソースにもありますが別物です。main.cの変数 c をいくら変更してもsub1.cの変数 c は変化しません。 static宣言をした変数はそのソースでのみ有効となります。
変数 d は計三ヶ所で定義されていますが全て独立しています。 これら、ブロックの内部で定義された変数は局所変数と呼ばれ、定義したブロック内でのみ有効です。 また、ブロックを抜けると値は捨てられます。
複数のソースから参照可能な変数が増えてくると、 各ソースの先頭に同じような変数宣言がずらずらとならぶことになります。 こうなると変数に関するちょっとした変更を行なうにも、全てのソースを修正しなければならず不便です。
そこで#includeを使います。これはソースのその部分に指定のファイルを埋め込む、という命令です。 この命令は関数のプロトタイプ宣言や変数宣言などの、多くのファイルから参照される情報を埋め込むのに使われます。
#includeの使い方には2種類あり、#include <ファイル名> 環境変数includeで示されるディレクトリから探すと、インクルードファイルの位置によって使い分けます。
#include "ファイル名" カレントディレクトリから探す
このようなインクルードファイル(慣習的に拡張子は.H)をカレントに置き、 各ソースファイルの先頭で、
================ action.h ================= int mx,my,dx,dy,bl,br; short c; char fname[22]; ===========================================#include "action.h"と記述しておけば、記述したすべてのソースから同じ変数にアクセス出来ます。
初期値付きの変数定義では少し細工が必要です。初期値付きの定義はどこか一ヶ所でのみ行なわなければなりません。 複数のソースで同一の変数を(初期値が同じであっても)初期値付きで定義することは出来ないのです。
では、どうすればいいのかということですが、いくつか方法があります。 そのうちのひとつを紹介しましょう。
#ifdef,#else,#endifというのが出てきましたがこれは#includeと同じプリプロセッサ命令です。 この場合「_MAIN_」というシンボルが定義されていたら#ifdef〜#else間を有効にし、 されていない時は#else〜#endif間を有効にします。
================ action.h ================= int mx,my,dx,dy,bl,br; #ifdef _MAIN_ //変数定義部 int birth=19720714; char fname1[22] = "HolyUp.dat"; char fname2[22] = "BirdShield.dat"; #else //変数宣言部 extern int birth; extern char fname1[22]; extern char fname2[22]; #endif ===========================================
これで、シンボルが定義されていたら初期値付き変数定義、シンボルが定義されていなければ 変数宣言をひとつのインクルードファイルで行なうことが出来ます。
例を挙げてみます。
main.cでは#define文でシンボル「_MAIN_」を定義しているので、初期値付きの変数定義がなされます。 sub1.cではシンボルが定義されていないので変数宣言が行なわれます。
================ main.c =================== #define _MAIN_ #include "action.h" .... .... =========================================== ================ sub1.c =================== #include "action.h" .... .... ===========================================
ソースを分割したらコンパイルです。分割したソースはリンカでひとつにまとめます。 よって、コンパイル作業はリンカを起動する直前で終わらせるようにします。 そのためのオプションは「-c」(小文字のみ)です。
gcc -c ファイル名(その他のオプションは場合に応じて付ける)でコンパイル、アセンブルを行い、オブジェクトファイルを作成します。 オブジェクトファイルはソースファイルの拡張子を「.o」に置き換えた名前になります。
すべてのファイルをコンパイルし、オブジェクトファイルが用意出来たら、 リンク作業を行ないます。
hlk -x -l main.o sub1.o sub2.o clib.l gnulib.l floatfnc.lコマンドラインから上のように打ち込むと、「main.x」という名前の実行ファイルが出来上がります。 この時エラーが出るならオブジェクトファイル、ライブラリの指定が足りないか、 変数の定義、宣言に誤りがあることになります。
まれに、オーバーフローというエラーを起こすことがありますが、元がC言語の場合 コンパイラのバグか、ライブラリのバグでしょう。制作者に連絡しましょう。
リンカにはいくつか、指定しておくと便利なコマンドオプションがあります。
「-x」のシンボルテーブルの削除ですが、デバッガによるデバッグ作業や、 ディスアセンブルを行なわないのであれば、削除した方がファイルサイズの節約になります。 ただし、消費メモリは変化しません。-x :シンボルテーブルを削除 -o filename :出力ファイルネームの指定 -l :環境変数libを参照する -i filename :インダイレクトファイルの指定
「-o」は出力ファイルネームを指定するオプションスイッチです。 これを指定しない時コマンドラインの先頭にあるオブジェクトファイルの拡張子を 「.x」に置き換えたものが出力されます。
「-l」ライブラリを検索する時環境変数libを参照します。
「-i」インダイレクトファイルの指定です。インダイレクトファイルとは、 リンクファイルが多すぎるなどの理由で、コマンドライン文字数が255文字を越えるような時に使われます。 コマンドラインで指定すべきファイル名、スイッチなどを、テキストファイルに記述し、読み込ませることが出来ます。
インダイレクトファイル名を「action.ind」とすると例にあげたリンク作業は
hlk -i action.indと、これだけで済みます。インダイレクトファイルの内容は
このようになります。
=============== action.ind ================= -x -l main.o sub1.o sub2.o clib.l gnulib.l floatfnc.l ============================================
分割コンパイルについての説明は以上で終わりです。変数の定義にさえ注意すれば、 何も難しいことはありません。早速挑戦してみましょう。
「make」とは、コマンドラインからmake
と打ち込むだけでコンパイル作業を全て行なってくれるありがたいツールです。 バッチファイルでコンパイルを行なう場合との相違点は、 ソースを修正した時必要なファイルのみを再コンパイルしてくれるという点です。
もっとも、それなりの設定ファイルを用意する必要がありますが。設定ファイルは、 「Makefile」という名前をつけるのが一般的です。(拡張子はなし)
要はMakefileの書き方を覚えればすべてうまくいくというわけです。 かなり複雑なことも出来るのですが、ここでは基本的なことしか説明しません。 私自身必要最低限の知識しかもっていないので。
では早速、説明に入りましょう。
Makefileの基本はこれです。「作成ファイル名」はその名の通りです。「依存ファイル名」は、 そこに記述されたファイルが更新されたとき「実行コマンド」を実行する、依存関係を表します。 ファイル名には複数のファイルを指定することが出来ます。「実行コマンド」は、 必ずタブの後ろに書かなければなりません。作成ファイル名 : 依存ファイル名 実行コマンド
例を挙げてみましょう。
この場合action.xとaction.oのタイムスタンプを比較し、action.oの方が新しければ 「hlk action.o」をコマンドとして実行します。action.xの方が新しければコマンドは実行されません。
action.x : action.o hlk action.o
action.c、action.h、cell.hのどれかが更新されていた場合コマンドを実行します。
action.o : action.c action.h cell.h gcc -c action.c
makeは「all」の右側にある「action.x」を完成させることを目的とします。 action.xはaction.oから作られ、action.oはaction.c、action.h、cell.hから作られます。 よってaction.c、action.h、cell.hのいずれかが更新されるとaction.xが新規作成されます。
all : action.x action.x : action.o hlk action.o sfxvilib.l sfxvirun.l action.o : action.c action.h cell.h gcc -c action.c
分割コンパイルをmakeで行なわせる場合はこのようになります。
all : action.x action.x : main.o sub1.o sub2.o hlk -o action.x main.o sub1.o sub2.o sfxvilib.l sfxvirun.l main.o : main.c action.h cell.h gcc -c main.c sub1.o : sub1.c gcc -c sub1.c sub2.o : sub2.c gcc -c sub2.c
ひとつ前のサンプルとまったく同じ動作をします。 こちらの方が同じような部分がまとめられているのですっきりしています。
all : action.x action.x : main.o sub1.o sub2.o hlk -o action.x main.o sub1.o sub2.o sfxvilib.l sfxvirun.l %.o : %.c gcc -c $< main.o : main.c action.h cell.h
「%.o : %.c」ですが、ファイル名が拡張子以外は同じであるという依存関係を意味しています。 「$<」はコマンド実行時に「:」の右側のファイル名に置き換えられます。
「all」により最終目的はaction.xです。action.xはmain.o、sub1.o、sub2.oから作られます。 main.oはmain.c、action.h、cell.hに依存しているのでどれかが更新されるとコマンドが実行されます。 が、コマンドが記述されていません。 この場合「main.o : main.c」が「%.o : %.c」にマッチするので「gcc -c $<」が実行されます。 「$<」には依存ファイル群の先頭のものに置き換わるので「gcc -c main.c」がコマンドラインに渡されます。 sub1.o、sub2.oは依存ファイルすら書かれていませんが 「%.o : %.c」によりそれぞれsub1.c、sub2.cが依存ファイルとみなされます。
ほかに覚えておくと便利なことは、継続行、マクロ設定、ファイル名置き換えです。
分割コンパイルで依存ファイル数が多くなりすぎた時など、一行に書き切れなくなってきた場合、 継続行指定を行なえば複数行にまたがって指定することが出来ます。
バックスラッシュ(もしくは円マーク)「\」の直後の改行は無効化されるので、 次の行も依存ファイルに含まれます。action.indの後ろにバックスラッシュがつかないことに注意。
action.x : main.o\ sub1.o\ sub2.o\ action.ind hlk -i action.ind
例の場合は
これと同じことになります。
action.x : main.o sub1.o sub2.o action.ind hlk -i action.ind
マクロは活用範囲が広いのでいろいろなことに使えます。マクロ名 = 定義文で設定し、
$(マクロ名)で、その位置に展開されます。
FLAGS = -O -c -I \develop\include %.o : %.c gcc $(FLAGS) $<
このように書かれていた場合、$(FLAGS)の位置にFLAGSの内容が展開されるので
gcc $(FLAGS) $< → gcc -O -c -I \develop\include $<のように置き換えが行なわれます。
ファイル名の置き換えですが、これにはいくつか種類があります。本当はほかにもあるのですが、私は知りません。$@ 作成ファイル名 $< 依存ファイル名 $^ 依存ファイル名(連結)
実行コマンドを記述する位置にこれを使うことで実行時に任意のファイル名に置き換えることが出来ます。
それぞれの使い方を例を挙げて解説します。
コマンドの三行目に「$@」が使われています。これはscene1_1.picに置き換わります。
scene1_1.pic : yakko1.pic back.pic apicg yakko1.pic apicg -pile back.pic apicg -s0,0,255,255 $@
コマンド実行時に「$<」はaction.cに置き換わります。action.o : action.c gcc -c $<
コマンドはhlk fire.o bomber.oに置き換わります。fire.x : fire.o bomber.o hlk $^
以上のことを踏まえてSFXVIキャラ制作用のMakefileの例を挙げます。 これが理解出来れば、Makefileの書き方は覚えられたようなものです。
=============== Makefile =================== all : action.x LIBS = \develop\lib\sfxvilib.l \develop\lib\sfxvirun.l floatfnclib.l FLAGS = -O -c -I e:\develop\include OBJS = act_main.o\ act_stand.o\ act_crouch.o\ act_sp_attack.o\ act_sp_shoot.o\ act_sp_move.o\ act_throw.o\ act_jump.o\ act_command.o action.x : $(OBJS) hlk -l -o $@ $^ $(LIBS) %.o : %.c gcc.x $(FLAGS) $< act_main.o : act_main.c action.h cell.h ============================================
リンカの指定がやや冗長ですが、サンプルなのでこうしてあります。 実際にはインダイレクトファイルを使った方がすっきりするでしょう。
Makefileの書き方がわかったら、 makeの使い方です。makeはカレントにあるMakefileを読み込んでそれに従って作業を行ないます。 よってMakefileはカレントディレクトリに用意します。
Makefileに別の名前のファイルを使う場合は「-f」オプションに続けてファイル名を指定します。
ファイルの新旧に関わらず作業を行なわせるには、「ーW」でファイル名を指定するか すべてを更新する「-a」をオプションとして与えます。
オプションをつけずにファイル名だけを指定すると、そのファイルを作成することを目的として、 作業を行なわせることが出来ます。
注意することは以上です。makeは基本的にMakefileさえ用意すれば、 あとはよきにはからってくれます。どんどん活用しましょう。