第 10 章分割コンパイル 1 ソースを分割する今まで出てきたソースは全て一つのソースファイルにソースを記述してきました しかし ソースが長くなっていくと全てを一つのファイルに書くと読みづらくなります そこで ソースを複数のファイルに分割してコンパイルを行う分割コンパイルをします 今章は章名にもなっている 分割コンパイルの方法についてやります 分割コンパイルする時は大抵 関連性のある機能ごとにファイルにまとめます そうすることによって ソースが長くなった時に複数のファイルに分け 更に機能ごとにまとまるので修正する時など読みやすくなります 良く使う処理を別のファイルにしていろんなプログラムで使いまわすということも考えられます また 変更を加えたファイルだけコンパイルすることが出来るので コンパイルの時間を減らすことが出来ます 更に 現在プログラムが作られる時は大抵複数人で行います そういう時ももちろん分割コンパイルします 分割コンパイルは次のセクションで述べる変数のスコープ ( 有効範囲 ) をしっかり把握していれば難しいものではありません 2 変数のスコープ変数はどこで宣言するかによってどこで使えてどこで使えないかが決まります 例えば今まで関数内で宣言した変数は関数内でしか使えませんでした これに対し 関数の外で宣言するとそのファイル内全体で使用することができます 関数内でしか使えないような変数をローカル変数 ファイル内全体で使用することが出来る変数をグローバル変数と呼びます 変数のスコープ int a; int b; void func(void){ int c; 上のようなソースだった場合 a は main 関数でも func 関数でも使うことが出来ます それに対し b は main 関数のみ c は func 関数のみで使うことが出来ます グローバル変 1
数はファイル内のどの関数からでも参照できるので便利ではありますが 変数の衝突が起こったり ファイル内のどこで値が書き換えられたかわかりづらくなったりなどの欠点があります 複数の関数で変数を共有する時は出来るだけ引数を使うようにし グローバル変数は プログラムの全体の状態を表すものなど最低限のものに留めておきましょう ( 最初はよく使うことになるでしょうが ) 先ほどグローバル変数の説明にファイル内全体で使えると書きました 今章ではソースの分割を行いますが 複数のファイルで変数を共有する時も出てきます その場合はどうするかというと その変数を共有したいファイル内で extern 宣言と呼ばれる宣言をします 複数のファイルで変数を共有する ファイル 1 int a; void func1(void){ ファイル 2 void func2(void){ ファイル 3 extern int a; void func3(void){ main 関数内や func1 関数内では当然変数 a は使うことが出来ます func2 では宣言がされていないので a は使うことは出来ません func3 ではファイル 3 内で extern 宣言をしているのでファイル 3 内では普通にグローバル変数として用いることが出来ます int a; と書くと a という変数のためにメモリ領域を取るなどが行われます extern int a; と書くと他のファイルにある a という変数をこのファイル内で用いるということを宣言しています 関数のプロトタイプ宣言も本来は頭に extern をつけるのを省略しているだけで 意味合いは同じです 2
3 Makefile それでは 具体的に分割コンパイルをする方法を見てみましょう 分割コンパイルを行うには Make と呼ばれる作業が必要です Make を行うには何通りか方法があります まずは 全てを手作業で行う方法です これについては後ほど触れます 他に Makefile と呼ばれるものを使う方法があります Visual C++ などの統合環境を用いると勝手に作ってくれるのですが まずは自分で書いてみましょう それでは例を示します Makefile を使った分割コンパイルの方法 ( プログラム 10.1-1) ファイル名 :main.c void func(void); func(); return(0); ファイル名 :func.c void func(void){ printf(" 関数が呼び出されました "); まずはコンパイルするソースを作ります 今までと違って今回はファイル名を指定していますが 別の名前でもかまいません ( その場合は次に示す Makefile の所でファイル名を用いますので各自ファイル名を置き換えてください ) main 関数内で func 関数を呼び出すので関数のプロトタイプ宣言を行います これはファイルを分割してもしなくても同じことです また それぞれのファイルで stdio.h をインクルードしています ヘッダーファイルをインクルードする時はそれぞれのファイル内で使う関数を使うのに必要なヘッダーファイルをそれぞれインクルードします ( この例だと main 関数では必要ないですが ) 分割したファイルのどこかでインクルードしたら別のファイルでも使えるということは無いので注意してください それでは次に Makefile の作り方を示します ファイル名は makefile にしておいて下さい 3
Makefile を使った分割コンパイルの方法 ( プログラム 10.1-2) # makefile for main # target : main.exe all : main.exe main.exe: main.obj func.obj bcc32 -emain.exe main.obj func.obj main.obj: main.c bcc32 -c main.c func.obj: func.c bcc32 -c func.c # から始まる行はコメント文を表します target は CPad に実行ファイルがどれかを知らせるための文で Make には関係ありません all はシンボリックターゲットと言って その後ろにあるファイル名が最終的に作られるファイルです 二つ以上の実行ファイルを作る時はその数だけ実行ファイル名を書きます その下からは実際に Make する時のファイルの依存関係と Make するためのコマンドです 依存関係は必ず行頭から書き コマンドは行頭に必ずタブ文字を入れてから書きます スペースを入れても動きません ところで.obj という拡張子があります これはオブジェクトファイルと言うものです 今まで特に触れませんでしたが C のソースを実行形式にする場合二つの手順が踏まれています コンパイラがソースをコンパイルした物がオブジェクトファイルです ソース内で使われているライブラリ関数を使用するのに必要なものと先ほどのオブジェクトファイルをつなげて実行形式にするのがリンクと言う作業です これが今までコンパイルと呼んでいた作業です main.exe: main.obj func.obj は main.exe が main.obj と func.obj から出来ているというファイル同士の依存関係を示します bcc32 -emain.exe main.obj func.obj はコマンドです 今まで CPad を使っていて意識していませんでしたが 本来 bcc はコマンドラインで使うコンパイラです このコマンドは実際に Make する時のコマンドと同じです -emain.exe はコンパイルする時のオプションで main.exe という名前の実行ファイルを生成するという意味になります この時 -e と main.exe の間にスペースを入れると動作しません 残りの二つは引数で main.obj と func.obj を用いると言うことを意味します 残りの文も基本的には同じ意味です -c は C のソースからオブジェクトファイルを生成するためのオプションです ちなみに この Makefile は BCC を用いた場合の物で他のコンパイラを使うと少し書き方が変わります 4
先ほど Make は全て手作業で行えるといいましたが Makefile に書いたコマンドを実際にコマンドラインで実行すると Make することが出来ます 今回の場合は bcc32 -c main.c と bcc32 -c func.c を実行した後に bcc32 -emain.exe main.obj func.obj を実行すると Makefile を用いた時と同じ実行ファイルが出来ます 4 自作ヘッダーファイルヘッダーファイルは自分で作ることが出来ます ヘッダーファイルは関数や変数の宣言のみが記載されたもので ファイルの分割を行う際に用います 先ほどの例だと main.c の void func(void); の宣言は普通ヘッダーファイルに書きます それではヘッダーファイルを用いた場合の分割例と Makefile の書き方を示します Makefile を使った分割コンパイルの方法 2( プログラム 10.2-1) ファイル名 :main.c #include func.h func(); return(0); ファイル名 :func.c void func(void){ printf(" 関数が呼び出されました "); ファイル名 :func.h void func(void); 先ほどの例と違う点は main.c で読み込むヘッダーファイルが増えている点と 関数の宣言がヘッダーファイルに移っただけです ヘッダーファイルをダブルクォーテーションで囲んでいますがこれは自作ヘッダーファイルをインクルードする時は普通ダブルクォーテーションで囲みます これは コンパイラが<>で囲んだ場合は標準ライブラリから探し で囲んだ場合は同じフォルダないから探すといった動作をするからです また 見てすぐに自作なのか標準ライブラリにあるのかわかりやすくする意味もあります ヘッダーファイルはこのように関数の宣言をそのまま書いたものです しかし 普通は関数の宣言が 2 重にならないようにプリプロセッサを用いて条件コンパイルさせます 5
多重インクルードを防ぐヘッダーの書き方 ( プログラム 10.2-2) ファイル名 :func.h #ifndef _func #define _func void func(void); #endif //_func 一見意味のないように見えるかもしれませんが こうする事によって同じヘッダーファイルを分割コンパイルする複数のファイル内で宣言されても関数の宣言が 2 重になりません stdio.h 等最初から用意されているヘッダーを見てみれば似たような記述があるはずです 条件コンパイルを忘れているかもしれませんので軽く復習すると意味は 1 行目が _func がマクロ定義されていなければコンパイルすると言う意味で 2 行目は普通のマクロの定義です 最後の行は条件分岐の最後につけるものでした これで このヘッダーファイルが呼び出されたのが 2 回目であれば既に _func がマクロ定義されていてインクルードされません ところで #endif の後ろにコメントがありますが マクロは大抵入れ子になってもインデントしないので どれと対応するのかがわからなくなるためこのように書くことがあります このような短いヘッダーファイルならあまり意味はありませんが 長くなった時には必須とも言えます それでは次に Makefile の書き方を見てみましょう Makefile を使った分割コンパイルの方法 ( プログラム 10.2-3) # makefile for main # target : main.exe all : main.exe main.exe: main.obj func.obj bcc32 -emain.exe main.obj func.obj main.obj: main.c func.h bcc32 -c main.c func.obj: func.c bcc32 -c func.c 先ほどと違うのは main.obj のファイルの依存関係に func.h が追加されているだけです これは書かなくても動きますが 普通はファイルの依存関係を示すために書きます 6
5 統合開発環境分割コンパイルを行う際に Makefile を自動的に作ってくれるソフトがあります 統合環境には有名なもので VisualC++ があります 他にも今まで使ってきたコンパイラを使う BCC Developer があります 別途使い方を示しますので今回のサンプルプログラムをコンパイルして使い方を確かめてください 6 10 章のまとめ大きなプログラムを組む時に分割コンパイルは必須になります 今のうちにある程度なれていた方がいいかもしれません また 変数のスコープの考え方は大変大事な物なので忘れないようにして下さい 確認問題 分割コンパイルを用いたプログラムを作りなさい (Makefile 統合環境両方でやる事 ) 余談とおまけ分割コンパイルとヘッダーファイルを使うと 実際のソースを隠す事が出来ます 実際標準ライブラリ関数はオブジェクトファイルの状態で提供されています その時に関数の仕様を表すのがヘッダーファイルです ヘッダーファイルはソースを読む時の手がかりにもなるのでそういう事を意識して書くようにしましょう また分割コンパイルがなくても出来ますが 関数ラッパという手法があります これは 既にある標準ライブラリ関数に引数を渡す前に自分で処理を挟む事が出来ます 標準ライブラリ関数は渡す引数によっては問題を起こす時もあるので引数のチェックをするべきですが それを関数として作ってライブラリにしておくと 再利用する事が出来て便利です 関数ラッパの詳しい事については自分で調べて見てください 7