分割コンパイルについて
少し簡単にまとめました.
とにかく簡単に作りたい!
main関数と関数群の入ったソースファイルだけ作っておけば充分と考えたくなるかもしれ
ませんが,それは課題で作るソースコードが短く,あまり再利用しない状態であるからだと
言えます.
また,オブジェクト指向とあらためて言われなくても,処理(小さな仕事)単位で関数(C++で
あればクラス)にまとめておき,その関数を持ってくるだけで,やりたい処理全体が簡単に
記述できればどれだけ便利か想像できると思います.
おなじみ Hello, world! をソースファイルひとつで記述すると以下のようになります.
main.cpp | ソースファイル名 |
#include <stdio.h> void prtHW(void); int main() { prtHW(); return 0; } void prtHW(void) { printf("Hello, world!\n"); ] |
宣言・制御文など 関数の宣言 main関数 関数の呼び出し 関数の実装 関数の中身 |
シンプルなソースファイルですが,右の列のような構造となっていることに注目して下さい.
のすべてがソースファイルに含まれています.もちろんこのままでもコーディングは可能
なのですが,関数が増えてくると段々と扱いが煩わしくなってしまいます.
理想的には,main関数内でやらせたい流れを記述しておき,関数の中身はどこか別の
場所に置いておければ便利ではないかと感じるのではないでしょうか?
上の例では関数が1つで僅か4行しかないので何も感じないと思いますが,例えば関数が
100あってそれぞれ100行くらいあるとやはり,100行の宣言と100×100行の関数の間に
埋もれてmain関数があるとやはり不便を感じると思います.
そこで,関数の宣言と関数の実装に間してmain関数のソースファイルからこれらを分ける
のが分割コンパイルの旨みとも言えます.
ヘッダファイルと呼ばれるファイルを使うと,元のソースファイルから一部を移動して記述
することができます.
simple.h | ヘッダファイル名 |
#ifndef _SIMPLE_H #define _SIMPLE_H void prtHW(void) { printf("Hello, world!\n"); } #endif |
一度コンパイルしたら, 繰り返さないための制御 宣言した関数 関数の中身(実装) はじめの制御の終わり |
やりたい処理について関数をprtHWと決めてヘッダファイルに置いてしまいました.元の
ソースファイルは以下のように,
main_simple.cpp | ソースファイル名 |
#include <stdio.h> #include "simple.h" int main() { prtHW(); return 0; } |
宣言・制御文など ヘッダファイルの呼び出し main関数 関数の呼び出し |
となりますので,随分すっきりしたと感じませんか?main関数の中は実行したい関数だけを
記述しているので,プログラムの流れだけを示していると言えます.
これだけでヘッダファイルを利用した分割コンパイルが可能となったと言えます.
もう少し,分割コンパイルらしい構造にしたい!!
さて,分割コンパイルを行う理由について考えてみましょう.上記では,作成した関数が多く
なるとソースファイルが大きくなって見辛いからとしましたが,それだけではありません.
作ったプログラムが正しく機能するようになった場合,そのまま仕様を固めてしまいたくなる
のです.特に複数人でプログラムを作成する場合,自分自身担当の部分が完成したことが
分かったら,[コミット]と言って,バグが見つからない限りは改変しないことが求められるの
です.何故なら,作成した関数についていつでも同じように使えることが重要で,再現性や
再利用性が望まれるからなのです.
このような理由から,関数の中身(実装)は改変されないように別ファイルすることがよく,
ヘッダファイルは,どんな関数が用意されたのかが分かるだけの見出し的なものにしておく
ことでこれが実現できるのです.
この考えに基いてヘッダファイルを記述すると,以下のようになります.
typical.h | ヘッダファイル名 |
#ifndef _TYPICAL_H #define _TYPICAL_H void prtHW(void); #endif |
一度コンパイルしたら, 繰り返さないための制御 宣言した関数 はじめの制御の終わり |
ヘッダファイルの中身はこれだけです.関数がどんな形で宣言されているかが分かるだけと
なっています.main関数でどのように呼び出せばいいのか分かるようにしておけばよいように
するため,適宜コメント文で説明を加えておけばよいのです.
もちろん,関数の中身は別途記述する必要があり,実装ファイルと呼ばれる,main関数のない
ソースファイルを作ります.
typical.cpp | ヘッダファイルのソースファイル名 |
#include <stdio.h> #include "typical.h" void prtHW(void) { printf("Hello, world!\n"); } |
宣言・制御文など 対応するヘッダファイル名 関数の中身(実装) |
実装される関数の中身は,別途ソースファイルに記述することになります.対応しているヘッダ
ファイル名が書かれていることと,main関数がないことが特徴です.
余談ですが,ヘッダファイル名とヘッダファイルのソースファイル名を一致させる必要はないので
これらの関連性を分かりにくくして秘匿性を上げたり,あるいは同じ機能の関数を複数の人で作
成したものを置換して比較したりするのにも使えるなど工夫の余地があります.
なお,main関数の格納されたソースファイルは以下のようになっています.
main_simple.cpp | ソースファイル名 |
#include <stdio.h> #include "typical.h" int main() { prtHW(); return 0; } |
宣言・制御文など ヘッダファイルの呼び出し main関数 関数の呼び出し |
特に変わりはないことが分かると思います.
ソースファイル,ヘッダファイル,実装ファイルを統合した実行ファイル(exeファイル)の作成.
上記をまとめると,
[作るべきファイル]
これらだけ作れば,分割コンパイルにより,プログラムの移植性,再利用性が上げられる
のです.もっとも,makefileに関する予備知識がないと全くできないので本ページで説明
します.
makefikeについて
複数のソースファイルを組み合わせてプログラムを作る場合、コマンドラインで一つ一つ
命令を打ち込むのは大変面倒なのと、必要な情報を忘れる可能性があります。
そのため、makefileというマクロ集を作っておき、開発しているプログラムに関連するような
ファイルを明らかにしておく必要があります。
情報処理演習1ですでに学んでいるかもしれませんが、もう一度説明しておきます。
一見面倒そうですし,難しく感じると思いますが,大変便利なものでもあるので,
用意したmakefikeは以下のものがあります。書き方はいくらでもあるので参考程度に見て
ください。
太字は自身のソースファイルやヘッダファイルの名前に合わせて変えるところです.
CC = bcc32 [1] LINK = bcc32 [2] TARGET =main_typical [3] OBJ =main_typical.obj typical.obj [4] .SUFFIXES: .obj .cpp .h [5] $(TARGET).exe:$(OBJ) [6] $(LINK) -e$* $** [7] .c.obj: [8] $(CC) -c $< [9] $(TARGET).obj: $(OBJ:%.obj=%.h) [10] typical.obj: typical.cpp typical.h [11] clean: [12] $(RM) $(TARGET) $(OBJ) *.tds *.bak [13] |
[1]はコンパイラとしてbcc32を使う。(このままで良い)
[2]はリンカとしてbcc32を使う。(このままで良い)
{3}は実行ファイル名としてkadai15fftとする。(名前を変えて下さい)
[4]は生成されるオブジェクトファイル群を示している。(名前を変えて下さい)
[5]はコンパイルに関連する拡張子を明言している.(このままで良い)
[6]はターゲットのファイル名とそれに関するファイル名を示す。(このままで良い)
[7]は実際のコマンドを示す。$@はターゲットの名前、$^はすべての関連するファイルの名前。(このままで良い)
[8]はサフィックスルールにより、*.cから*.objファイルを生成することを示す。(このままで良い)
[9]は実際のコマンドを示す。$<は関連するファイルを最初のファイルの名前。(このままで良い)
[10]は実行のヘッダファイルの関連性を示す。(書かなくてもいいが,確実なリンクにはあった方がいい)(このままで良い)
[11]はヘッダファイルの関連性を示す。(書かなくてもいいが,確実なリンクにはあった方がいい)(名前を変えて下さい)
[12]は不要ファイルを消去することを示す。(書かなくてもいい)(このままで良い)
[13]は実際のコマンドを示す。RMはすでに用意されたマクロ。(書かなくてもいい)(このままで良い)
ソースファイルから実行ファイルができるまでを考えてみましょう。
ソースファイル一つだけであれば、ソースファイルがコンパイルされてオブジェクトファイルが
生成され、オブジェクトファイルとライブラリがリンクされて実行ファイルが生成されます。
これだけであれば、例えば,
bcc32 main.c
だけでもコンパイルからリンク、実行ファイル生成までやってくれますが、分割コンパイルには
対応できません.そこでmakefileに関連するファイルをすべて記述し,複数のファイルから構成
されるファイル群(プロジェクトといっても良い)から実行ファイルを作成します.これさえ作れば
make
とタイプすれば,実行ファイルを生成することが可能です.もしmakefile以外のファイル名にした
ければ,例えばmake_prg.mを作ったとしたら,
make -f make_prg.m
などと-fとファイル名を示せば,同様の処理が可能です.同一のディレクトリにいくつも開発する
プログラムを置いてしまう時には,こちらの方がいいかも知れません.
上記の例を基にMakefileを作るとすれば,以下のようになります.
CC = bcc32 LINK = ilink32 TARGET = main_typical OBJ =main_typical.obj typical.obj .SUFFIXES: .obj .cpp .h .cpp.obj: $(CC) -c $< $(TARGET).exe: $(OBJ) $(CC) -e$* $** main_typical.obj: main_typical.cpp typical.h typical.obj: typical.cpp typical.h |
如何でしょうか?ヘッダファイルに分けれだけで,再利用しやすくなり,また,一旦作って成功した
関数を誤って壊してしまうこともなく今後に利用できるので,便利さが分かって頂けたかと思います.