Next Top Next

第 08 回 ファイルの分割

プログラムがある程度大きく関数もたくさんになってくると、 そのすべてを一つのファイルに書くのは効率が悪い。 また何らかの汎用的な関数の集まりを用意した場合には、 それを複数のプログラムで共用したい場合もあるだろう。 この様な場合にはソースファイルの分割を考えるべきである。


ファイルの分割とヘッダファイル


ベクトルに関する基本的な操作を行う関数の集まりを書いたとしよう。 そしてベクトルを扱うプログラムが二つあったとする。 この場合、一方のファイルに書いてあるベクトルの基本操作に関する関数を他方にコピーして利用することは出来るが、 何らかの間違いに気付いたときには双方を修正しなければなず、 この作業は煩雑になる。 そこで共通に使う部分を別のファイルに書いておけば、 そのファイルを修正するだけで両方のプログラムにその修正が反映される。

具体的な例を見よう。次のような二つのファイルを用意する。 vector.c はベクトルに関する基本的な関数の集まりで、 sample.c はそれを利用するプログラムである。

/***** vector.c ****/
#include <stdio.h>

void vecadd(int n, float *a, float *b, float *c)
{
  int i;

  for (i = 0; i < n; i++)
    c[i] = a[i] + b[i];
}

void vecprint(int n, float *a)
{
  int i;
  printf("(");
  for (i = 0; i < n-1; i++)
    printf("%f, ", a[i]);
  printf("%f)\n", a[n-1]);
}
/***** sample.c *****/
#include <stdio.h>

int main(int argc, char *argv[])
{
  float x[2] = {1.0, 2.0}, y[2] = {2.0, 1.1}, z[2];

  vecadd(2, x, y, z);
  vecprint(2, z);

  return 0;
}
これをコンパイルするには
home$ gcc vector.c sample.c
と両方のファイル名を指定すればよい。

プログラムに問題がなければこれでよいのであるが、 この場合 main のコンパイル時には vecadd と vecprint の引数と戻り値の型が分からず、 そのチェックが機能しない。 そこで関数の所でも説明したように、その宣言

void vecadd(int n, float *a, float *b, float *c);
void vecprint(int n, float *a);
を main の前に記述することが好ましい。 しかし vector.c に多くの関数が定義されているような場合には vector.c を利用するすべてのプログラムにそのすべての関数の宣言を書くことは面倒であり管理も大変である。 そこで関数の宣言などを記述した別のファイルを用意する。 これをヘッダファイルといい *.h というファイル名にする。
/***** vector.h *****/
void vecadd(int n, float *a, float *b, float *c);
void vecprint(int n, float *a);
そしてこれらの関数を利用するファイルでこのファイルを include する。
/***** sample.c *****/
#include <stdio.h>
#include "vector.h"

int main(int argc, char *argv[])
....
stdio.h を include していたのも同じ理由である。 システムが用意したヘッダファイルを include するときにはファイル名を <...> でくくり、 自分で用意したファイルを include するときにはセミコロンでくくり "...." とする。 ヘッダファイルには通常、関数の宣言だけでなく、 マクロの定義、構造体の定義、大域変数に関する情報なども記述する。


分割コンパイル


プログラムが大規模になるとそのコンパイルにも時間がかかるようになる。 このとき分割されたファイルを個々にコンパイルすることによって、 修正されていないファイルを再コンパイルする必要がなくなり時間が短縮される。 一つのファイルをコンパイルするには

home$ gcc -c vector.c
のように gcc にオプション -c をつける。 これによってオブジェクトファイル vector.o が生成される。 vector.o は実行可能なファイルではない。 同様に sample.o も作成してから
home$ gcc vector.o sample.o
とすれば実行ファイル a.out が生成される。 このときにはコンパイルは行われておらず、 コンパイル済みのオブジェクトファイルを結合するリンクという操作が行われている。 vector.c を利用するプログラムが複数あるときには、 再コンパイルすることなしに vector.o を利用することが出来る。

課題 08.01
適当なプログラムを複数のファイルに分けて書き分割コンパイルせよ。


コンパイルの自動化 - make


ある程度大きなプログラムを書くようになるとコンパイル時のコマンドも複雑になる。 例えば

home$ gcc -O3 -lm a.c b.c c.c
などである。 これを毎回入力するのは面倒であり、ミスの原因ともなりうる。 そこでこの作業を簡単にするために make が利用できる。 Makefile という名前のファイルに
all : 
    gcc -O3 -lm a.c b.c c.c
と書き込んでおく。gcc の前はスペースではなくタブでなくてはならない。 このようにしておいて
home$ make
とすれば gcc -O3 -lm a.c b.c c.c が実行される。 コマンドは複数のものを並べて書くことが出来る。 同様のことは適当はシェルスクリプトを用意すれば出来るが、 make には他にも色々と便利な機能がある。 例えば修正されたファイルのみを分割コンパイルする等であるが、 ここでは詳しくは説明しないので自分で調べてほしい。

上の例を見よう。 まず、はじめの all というのはターゲットと呼ばれるもので、 複数のターゲットを設定できる。 単に make として実行すればはじめのターゲットが選ばれたものとし、 make <ターゲット名> とすれば指定したターゲットが選ばれる。

make はコンパイル以外の場合にも有効に活用される。 例えば LaTeX の文章を作成する場合に

all : 
    platex foo.tex
pdf :
    platex foo.tex
    dvipdfmx foo.dvi
などというファイルを Makefile として用意しておけば、pdf ファイルを作成する場合に make pdf とすれば良いことになる。

課題 08.02
適当な Makefile を書きなさい。


Next Top Next