第30回 プログラミングについて 『ソースファイルやオブジェクトファイルの管理』

『ソースファイルやオブジェクトファイルの管理』
皆さんはプログラムを作成するときにどのようにソースファイルやオブジェクトファイルの管理をしていますか?
基本的には、
(1)ソースファイルを作成または更新
(2)ソースファイルをコンパイルしオブジェクトファイルを作成
(3)オブジェクトファイルをリンク
の順番で行ないます。
私の場合はプログラムの規模にもよりますが、小さいプログラム(千行程度)ではメインのルーチンもサブのルーチンも1つのソースファイルで作ることが多く、大きいものはサブのルーチンはその機能の単位で別のファイルに分けて作ります(これも一般的だと思います)。1つのソースファイルでプログラムを作るときにはあまり問題はありせんが、複数のソースファイルで作成するときには各人各様のようです。
最近のパソコン用のコンパイラは統合開発環境といってソースファイルやオブジェクトファイルの管理はそれらのプログラムが行なってくれます。これも慣れるとなかなかの環境を提供してくれるようですが、実は私はこれには馴染めず使ってはいません。どうしても昔から行なってきた手法に馴染んでしまっているのでいまさら変えようという気が起きないのです。そこで今回は私のプログラム開発環境(おおげさですが)を紹介します。
私がプログラムを作るときのディレクトリの環境はおおよそ次のようにしています。
(特に複数のソースファイルにプログラムを分割して作るときには間違いなく行なっています。)
作業ディレクトリ
|
|
ソースの保存ディレクトリ
要するに作業ディレクトリの下にソースファイルの保存用のディレクトリを1つ用意しておきます。ソースファイルを作成したり変更したりするのは作業用のディレクトリで行い、完成したソースファイルは保存用のディレクトリに移動コピーします(通常はコピーを作成した後は作業用のディレクトリのソースは削除します)。また、既に作成して保存用のディレクトリに移動してあるソースファイルに変更を加えるときには必ず作業用のディレクトリにコピーしてから行ないます。
問題は出来上がった複数のオブジェクトファイルをどうやってリンクするかです。現在の殆どのコンパイラにはMAKEという便利なプログラムがあって、ソースファイルやオブジェクトファイルの関連を定義しておけば、変更された日時に応じて必要なファイルを再コンパイルして実行イメージを作ってくれます。これを愛用している方は多いかと思います。がしかし、私はこれも使っていません。
私は、リンカを使ってオブジェクトファイルとオブジェクトライブラリから実行イメージを作成しています。この手法にはこの手法なりの長所があるからです。
いま、
main.c sub1.c sub2.c sub3.c
の4つのファイルがあったとします。これらのファイルから実行イメージを作成するときにはどのコンパイラでもおおよそ次のように行ないます。以下DOSでの例で説明します。
cc main.c sub1.c sub2.c sub3.c
コンパイルとリンクを別々に行なうときには、
cc /c main.c
cc /c sub1.c
cc /c sub2.c
cc /c sub3.c
cc main.obj sub1.obj sub2.obj sub3.obj
として行ないます。コンパイラのオプションはUNIX系のものでは /c ではなく -cで、オブジェクトファイルにファイルタイプもUNIX系のものでは .obj ではなく
.o です。
もしこの手法で100個のファイルをリンクしようとしたときには気が遠くなってしまいます。これをバッチファイルにしてもOSによっては環境変数やコマンドの引数の長さの制限があったりするのでなかなかうまくはいきません。そこでオブジェクトライブラリが登場します。
オブジェクトライブラリとは、UNIXでは ar コマンドで作成されるアーカイバファイルです。VMSではLIBコマンドで作成されるオブジェクトライブラリ(.OLB)ファイル、DOSではLIBコマンドで作成されるライブラリファイル(.LIB)ファイルです。
オブジェクトライブラリを管理するコマンドは、コンパイルした後に出来上がるオブジェクトファイルを追加、置き換え、削除するなどの機能があります。そこで、上記のmain.obj 以外のオブジェクトファイルをオブジェクトファイル(ここでは obj.libとします)に追加しておくと、
cc main.obj obj.lib
とすることで実行イメージを生成することができます。このときにもコンパイラやリンカによってはコマンドに対する引数の指定方法やオプションが違う場合がありますので注意して下さい。
単純にはこれだけのことなのですが、オブジェクトライブラリを使う利点は別のところにあります。いま、 sub2.c を変更して動作テストを行ないたいとします。このときには sub2.c を別のファイル名でコピー(ここでは l.c とします )して変更を加え、このファイルをリンクします。
もし、リンクを行なうために l1.bat を
l1.bat :
cc main.obj sub1.obj sub2.obj sub3.obj %1 %2 %3 %4 %5
として実行イメージを作成していたとすると、
l1 l.obj
としたのでは、l.obj と sub2.obj が重複してしまいますのでリンク時にエラーが発生してしまいます。この場合には l1.bat 内の sub2.obj を削除してからリンクを行なわなければなりません。
オブジェクトライブラリを使用して実行イメージを作成するバッチファイルを使用するとすれば、l2.bat を
l2.bat :
cc main.obj %1 %2 %3 %4 %5 obj.lib
とし、
l2 l.obj
とすることで問題なく実行イメージを作成することができます。通常リンカはオブジェクトファイルが指定されると、必ずそれをリンクしますので重複エラーが発生することがありますが、リンクしなければならないオブジェクトが指定されたオブジェクトライブラリにあるときにはそれを使い、必要のないものはリンクされません。だからといって次のようなバッチファイルでは問題が発生します。
l3.bat :
cc main.obj obj.lib %1 %2 %3 %4 %5
このバッチファイルで j.obj を
l3 l.obj
としてリンクすると、l.obj と sub2.obj が重複してしまいます。これはリンカがオブジェクトをリンクするときには指定されたファイルの順番に検索していきますので、obj.lib を検索しているときに sub2.obj が発見されてしまいますので、更に l.objをリンクしようとして重複エラーが発生してしまうのです。このような問題が発生しないようにリンクする順番には気をつけなければいけません。
このように、私がオブジェクトライブラリを愛用している理由は既存のソースファイルを変更して動作テストするときに非常に便利だからなのです。これ以外の方法では、バッチファイルの内容を変更する必要があったり、MAKEを使用するときには sub2.cを別のファイル名でコピーしてから sub2.c を変更するか、MAKEの定義内容を変更する必要があるからです。オブジェクトライブラリを使うときにはオブジェクトファイルを指定したときにはそれをリンクし、指定しないときには正規のリンクを行ないますので気軽に動作確認ができるのです。
リンク時の話はこれくらいにしておき、次はヘッダファイルの指定方法を考えてみます。プログラムを分割して作成するときに、関数のプロトタイプや構造体の宣言、定数などが複数のファイルで同一のものを使用するときにはその部分を別のファイルにしてそのファイルをコンパイル時にインクルードするようにするのが一般的な手法です。
C言語で、
#include <stdio.h>
と記述すると、stdio.h とファイルがコンパイル時にインクルードされます。このインクルードの指定はおまじないのようによく記述しますが、このファイルはいったいどこにあるのでしょうか。普段はあまり気にしないでインクルードしていますが、このファイルはコンパイラによって固定されたディレクトリにあるファイルをインクルードします。通常では環境変数にそのディレクトリを設定しておき、コンパイラがコンパイル時にそのディレクトリを参照するようにしています。
それでは、
#include "stdio.h"
と <> の代わりに "" で囲んで指定すると、どのようなことになるでしょうか。この場合は現在のディレクトリにある stdio.h ファイルをインクルードします。このファイルがないときには、#include <stdio.h> と同じになります。
考えてみたいのは、このダブルクォーテーションで囲まれたファイルをインクルードする場合です。インクルードされるファイルがインクルードするファイルと同じディレクトリにあるときにはあまり問題がありませんが、違うときにはときどき面倒なことになってしまいます。
次の例をみてみましょう。
a.c :
#include "c:\work\include\def.h"
:
:
b.c
#include "c:\work\include\def.h"
:
:
c.c
#include "c:\work\include\def.h"
:
:
いま a.c、b.c、c.c という3つのソースファイルが a:\work\include\def.h をインクルードするように記述されているとします。これらのファイルを別のパソコンでコンパイルしようとすると、 a:\work\include というパスに def.h というファイルがなければなりません。ないときにはそのディレクトリを作って def.h ファイルをそこに置かなければなりません。もしそのパソコンが b: までしかないときにはソースファイルを変更して別のディレクトリからインクルードするようにしなければまりません。もしソースファイルが100個もあったとすると、気の遠くなるような作業をしなければなりません。
また、このようなときもあります。
a.h :
#define ABC 1
:
:
b.h :
int data=ABC;
:
:
a.c :
#include "a.h"
#include "b.h"
:
:
この場合 a.c はコンパイル時に問題はおきませんが、
b.c :
#include "b.h"
#include "a.h"
:
:
とすると、data に ABC を代入しようとしたときに ABC が定義されていないのでエラーになってしまいます。また a.h をインクルードし忘れても同じエラーになってしまいます。
これらのような問題を防ぐにはどうしたらいいのでしょうか。いろいろと試してみましたがインクルードするファイルを1つにすることで解決できます。私は現在この方法で行なっています。具体的には、
a.c :
#define INCLUDE_DEF_H
#define INCLUDE_B_H
#include "include.d"
:
:
include.d :
#ifdef INCLUDE_DEF_H
#include "c:\work\include\def.h"
#endif
#ifdef INCLUDE_B_H
#define INCLUDE_A_H
#endif
#ifdef INCLUDE_A_H
#include "a.h"
#endif
#ifdef INCLUDE_B_H
#include "b.h"
#endif
のようにします。要するにソースファイルがダブルクォーテーションで囲んでインクルードするファイルを1つ(この場合は include.d)にし、インクルードしたいファイルは #define 文を使って宣言するのです。こうすると、ソースファイルが100個あってもこのファイル1つを変更するだけで済み、インクルードする順番も気にしなくてもよくなすのです。
他にもいい方法があるかも知れませんので色々と試してみる価値がありそうです。もしGOODな方法があったら内緒で教えて下さい。
それではまた次回。