Cによるプログラミング入門14
プログラムファイルの分割
今回は、プログラムファイルを分割する話です。いろいろなコンパイラについて説明できないので、Borland
C++ 5.5について説明します。もちろん、他のコンパイラでも、考え方は共通しています。
ただし、ここですることは、コンパイラ(実はリンカも含まれます)の詳細な説明ではなく、「考え方」の説明です。
とりあえず、例を見てみましょう。まず、次のようなファイルを2つ作って見てください。ファイルの名前は、コメントで示したものとします。
/* func.h */
int func(void);
/* func.c */
#include <stdio.h>
#include "func.h"
int func(void){
int a;
printf("好きな数字を入力してください。\n");
scanf("%d", &a);
return a;
}
このように書くと、func.hというファイルでfuncという関数を宣言し、func.cでfuncという関数の定義を書いていることになります。これで、func.cはコンパイルできます。まあ、とりあえず、コンパイルしてみましょう。それには、コマンドラインで、
bcc32 -c func.c
とします。ここで「-c」はコンパイルだけ行いリンクはしないという意味です。コンパイルやリンクに関してのだいたいの話は第1回でしました。これをざっと見なおしてください。ただ、第1回ではあまりイメージがわかなかったかもしれません。詳しくは、今回、わかると思います。
さて、コンパイルはしましたが、これでfunc.exeというファイルはできません。実行可能ファイルは、一般にリンクの結果できるもので、今はリンクをしていないからです。コンパイルの結果、実は、func.objというファイルができるのです。
コンパイルだけしてリンクをしなかった理由は、func.cにはmainがないからです。プログラムの中心はmainなのでした。これがないということは「実行できない」ということなのです。そのため、実行可能ファイルを作るように「リンク」をしても意味がないわけです。実際、無理にすると(つまり「-c」なしでbcc32をすると)、「mainがない」というエラーになってしまいます。
それでは、func.cに意味はないのかというそんなことはありません。
Fig.1 コンパイルとその結果。
「dir fun*」は「funではじまるファイルを表示せよ」という意味です。
func.objのようなコンパイルしてできるファイルをオブジェクトファイル(Windowsではobjファイル)などといいます。これは、func.cがコンピュータの言葉(バイナリ、つまり0と1)に翻訳されたものなのです。
なお、func.cには「#include "func.h"」という行があります。これは、「func.hというファイルをこの場所に持って来い(インクルードする)」という意味です。func.hはこのようにfunc.cファイルにインクルードされるので、特に、コンパイル時にファイル名を書く必要はないのです。
とにかく、func.objというファイルができました。funcの定義がバイナリで書かれているファイルです。この関数funcを使うには、たとえば、次のようなプログラムを書きます。
/* func_test.c */
#include <stdio.h>
#include "func.h"
int main(void)
{
int a = func();
printf("入力した数は%dですね。\n", a);
return 0;
}
最初の方に「#include "func.h"」とあるのは、インクルード文で、「func.hをここに持って来い」という意味でした。func.hにはfuncという関数が宣言されています(つまり、名前が書いてあるわけです)。これで、func_test.cでは、funcという関数が使えるように...まあ、なるのです。しかし、あと一ひねりあります。
まず、一応、トライしてみましょう。コマンドラインで
bcc32 func_test.c
としてみてください。エラーになるでしょう。しかし、エラーメッセージをよく見ると、普通のエラーではないようです。実は、これはコンパイルには成功し、リンクに失敗しているのです。(コンパイルとリンクの説明をきちんとしていないのでつらいところですが、もう少しがまんして読み進めてください。)
Fig.2 リンクエラー
これは、「funcという名前は(func.hのおかげで)わかったが、その定義がわからん」という意味なのです。実際、「dir fun*」などをすると、コンパイルは成功しているので「func_test.obj」ができていることがわかります。ただ、実行可能ファイル(.exeファイル)はできていないのです。
つまり、どういうことでしょうか。これは、次のように考えてください。
コンパイルすると、オブジェクトファイルができる。
オブジェクトファイルをつなげることをリンクと言う。
実行可能ファイルは、オブジェクトファイルをつなげたものである。
bcc32(というコマンド)には、コンパイルとリンクという2つの機能が含まれている。
(ただし、「-c」指定で、コンパイルだけすることもできる。)
上でbcc32が失敗したのは、リンクすべきファイル、つまりfuncの定義がバイナリで書いてあるファイルを示さなかったからなのです。
func_test.cも
bcc32 -c func_test.c
とすれば、コンパイルだけされ、エラー(リンクのエラー)にはなりません。しかし、これでは、いつまでたっても実行可能ファイルができないですね。
そこで、bcc32で、リンクすべきオブジェクトファイルも一緒に書けば、コンパイルだけでなくリンクにも成功し、実行可能ファイルができるのです。それは、次のようにします。
bcc32 func_test.c func.obj
これは、func_test.cをコンパイルしたものに、(funcの実際の定義が書いてある)func.objをリンクし、func_test.exeを作る命令なのです。
Fig.3 コンパイル・リンク・実行。
さあ、これで、プログラムファイルの分割ができるようになりました。
...と言われても、ぴんと来ないかもしれません。これは何のためにあるのでしょう。
まず、言っておくと、コンパイル・リンクの仕方は、いくつかあります。たとえば、まず、はじめにfunc.objを作らなくても、
bcc32 func_test.c func.c
とすると、2つのファイルがコンパイルされ、できたオブジェクトファイルがリンクされ、実行可能ファイルが作られます。あるいは、最初に「-c」で、func.cとfunc_test.cを別々にコンパイルし、func_test.objとfunc.objを作り、それから
bcc32 func_test.obj func.obj
とすれば、今度はリンカ(リンクするソフト)だけが働き、実行可能ファイルができます。
どちらがいいでしょう?自分でちょっとしたプログラムを書いて実験してみるときは、私は、上のようにやります。しかし、大勢の人が協力して大きなプログラムを書いているときはどうでしょう?
たとえば、func.cもfunc_test.cも、長い長いプログラムだったとしましょう。そのような場合、1つのファイルに書くより、手分けして2つのファイル(func.hもいれると3つのファイル)に書いておいた方がよいでしょう。一人でやっているときでも、ファイルを分割した方がよさそうですよね。その方が、見やすいはずです。
そのような場合、ファイルを1つ1つコンパイルしておいて、あとでリンクした方がよいでしょう。コンパイルすることで、単純なミスは見つけられるわけですし、プログラムが長くなればコンパイルの時間もそこそこにかかるようになるからです。
つまり、ファイルの分割は、大きなプログラムを書くためのものなのです。今回の例では、funcという関数を1つしか書きませんでしたが、普通は、いくつかの関連ある関数を、1つのhファイルと対応するcファイルに書き込むと思ってください。もちろん、ファイル名と関数名を一致させる必要もありません。
一般に、hファイルはヘッダファイルなどとよばれ、対応するcファイルはソースファイルなどともよばれるのですが、ヘッダファイルには関数の宣言を、ソースファイルには関数の定義を書くわけです。
こうしておけば、ヘッダファイルとオブジェクトファイル(ソースファイルをコンパイルしたもの)を受け取った人が、自分のプログラムにヘッダファイルをインクルードし、オブジェクトファイルをリンクして使うことができます。なかなか便利な仕組みではないでしょうか。
ところで、Cのプログラムの最初におまじないとして、いつも
#include <stdio.h>
と書いてきました。実は、これはstdio.hというファイルがどこかにあり、そこにprintfなどの関数の宣言があるからなのです。ファイル名を「""」ではなく「<>」で囲んでいるのは、このファイルが「コンパイラ・リンカシステム」に組み込まれている特別なものだからです。特別なものなので、対応するオブジェクトファイル(通常はオブジェクトファイルがまとめられ、ライブラリファイルなどとよばれるものになっていますが、本質は同じです)は、自動的にリンクされるようになっているので、気にする必要もなかったのです。
元気がある人は、コンパイラがインストールされているフォルダ(ディレクトリ)あたりをさぐって見てください。stdio.hというファイルがあるはずです。中をのぞいてみると、まだまだ知らないキーワードがあってがっかりするかもしれませんが、あまり気にする必要はありません。printfの宣言などもあります。それから、何より、絶対書き変えないように。
Fig.4 stdio.h探し(Borland C++の場合)
最後にコメントが1つあります。同じヘッダファイルを複数回インクルードすると、そのままでは、インクルードしたファイルに同じコードが複数書き込まれることになります。実は、関数の宣言などは、複数回書き込まれても問題ありません。しかし、一般のヘッダファイルの中に、「複数回書き込まれるとエラーになるコード」が書いてないとは限りません。
そのような場合、同じヘッダファイルを複数回インクルードしないように気を付ければよいかというと、なかなか簡単ではないのです。そこで、「ヘッダファイルを複数回インクルードしても大丈夫にする方法」があるのです。
それには、たとえば、func.hを次のようにします。
#ifndef FUNC_H__
#define FUNC_H__
/* func.h */
...
...
#endif
ここで「#ifndef FUNC_H__」は、「FUNCH__というものが定義されていなければ、続けて以下を読み込め」という程度の意味です。この「以下」とは「#endif」までのことです。FUNC_H__というのは、func.hというファイルを識別するために、私が勝手に考えたものです。もちろん、コンパイラ・リンカのシステムが使っている名前を使うと思わぬエラーになりますが、そうでなければなんでもかまいません。
普通は、FUNC_H__などというものは、使われていないはずですから、このファイルがインクルードされると、「#endif」まで読み込まれます。その最初にあるのが「#define
FUNC_H__」ですが、これは「FUNC_H__というものをここで定義せよ」という意味になります。これで、以降、FUNC_H__が定義されるわけですが、これ自身がどうということはありません。続けて、ファイルの中身が読み込まれて終わりです。
それでは、あるファイルで間違って、2度(以上)func.hをインクルードしたとしたらどうなるでしょう。まず、最初のインクルードでは、FUNC_H__が定義され、ファイルの中身が、インクルードしたファイルに書き込まれます。2度目のインクルードでは、最初のインクルードでFUNC_H__が定義されているので、「#ifndef
FUNC_H__」で条件が満たされず、それ以下は読み込まれない(したがって、書き込まれない)のです。この結果、func.hを2度(以上)インクルードしても、同じコードが繰り返し書き込まれることはなくなるのです。これは、ヘッダファイルを作るときの常套手段になっています。
さて、これでC入門はおしまいです。お疲れ様。(^^)
課題14 適当なプログラムを複数のファイルに分割してみてください。