課題7 マクロ定数+Makefile+へッダファイル

マクロ定数(記号定数)

下のプログラム main.c は OpenGL と GLUT を用いて何らかの2次元図形を描くプログラムです。 ただし、どんな図形を描くのかまだ決めていません。

main.c

まず、このプログラムをダウンロードしてください。

Netscape でファイルをダウンロードするには、 上のリンクのところ(文字の色が変っているところ)で、 キーボードの [Shift] キーを押しながら マウスの左ボタン を押してください。

ダウンロードできたら、 このプログラムをコンパイルしてみてください。

このプログラムを何も考えずにコンパイルすると、 "LOOPS", "WIDTH" および "HEIGHT" の3つの「識別子」が 未定義 だというエラーが出ます。

% cc main.c[Enter]
main.c: In function `display':
main.c:46: `LOOPS' undeclared (first use in this function)
main.c:46: (Each undeclared identifier is reported only once
main.c:46: for each function it appears in.)
main.c: In function `main':
main.c:159: `WIDTH' undeclared (first use in this function)
main.c:159: `HEIGHT' undeclared (first use in this function)

これらは本来、 プログラムの先頭で次のように定義すべき マクロ定数(記号定数)です。

#define WIDTH  512      /* 最初に開くウィンドウの幅     */
#define HEIGHT 512      /* 最初に開くウィンドウの高さ   */
#define LOOPS  10000    /* 計算回数(多いほど滑らか)   */

このエラーを無くすには、 プログラム中でこれらを定義してやる必要があります。 しかし、次のようなコマンドの オプション をつけてコンパイルすれば、 プログラムを修正せずに記号定数を定義することができます。

% cc -DWIDTH=512 -DHEIGHT=512 -DLOOPS=1000 main.c[Enter]

上のコマンドで main.c をコンパイルしてください。

これで、確かにこの3つの記号定数が未定義だという コンパイラのエラーは出なくなります。 しかし、その代りに 「"glなんとか" が見つからない」という リンカ のエラーが大量に出てしまいます。

/tmp/cctjyzoh.o: In function `display':
/tmp/cctjyzoh.o(.text+0xf): undefined reference to `glClear'
/tmp/cctjyzoh.o(.text+0x17): undefined reference to `glPushMatrix'
/tmp/cctjyzoh.o(.text+0x4c): undefined reference to `glRotated'
/tmp/cctjyzoh.o(.text+0x59): undefined reference to `glBegin'
/tmp/cctjyzoh.o(.text+0x95): undefined reference to `sin'
/tmp/cctjyzoh.o(.text+0xc4): undefined reference to `cos'

(途中略)

/tmp/cctjyzoh.o: In function `main':
/tmp/cctjyzoh.o(.text+0x353): undefined reference to `glutInit'
/tmp/cctjyzoh.o(.text+0x360): undefined reference to `glutInitDisplayMode'
/tmp/cctjyzoh.o(.text+0x375): undefined reference to `glutInitWindowSize'
/tmp/cctjyzoh.o(.text+0x385): undefined reference to `glutCreateWindow'
/tmp/cctjyzoh.o(.text+0x395): undefined reference to `glutReshapeFunc'
/tmp/cctjyzoh.o(.text+0x3a5): undefined reference to `glutMouseFunc'
/tmp/cctjyzoh.o(.text+0x3b5): undefined reference to `glutMotionFunc'
/tmp/cctjyzoh.o(.text+0x3c5): undefined reference to `glutKeyboardFunc'
/tmp/cctjyzoh.o(.text+0x3d5): undefined reference to `glutDisplayFunc'
/tmp/cctjyzoh.o(.text+0x3ee): undefined reference to `glClearColor'
/tmp/cctjyzoh.o(.text+0x3f6): undefined reference to `glutMainLoop'
collect2: ld returned 1 exit status

これは、このプログラムの中で使用している 関数が見つからないというエラーです。 これらの関数は、 このコンピュータに最初から用意されている ライブラリファイル の中に入っています。 ライブラリファイルの中に入っている関数を使うには、 コマンドのオプションでそのライブラリファイル指定する必要があります。

"glなんとか" という関数は OpenGL 関連のものですから、 さらに次のようなオプションを追加して、 OpenGL(と X Windows) のライブラリファイルを指定します。

-l の l はアルファベットのLの小文字です。
% cc -DWIDTH=512 -DHEIGHT=512 -DLOOPS=1000 main.c -L/usr/X11R6/lib -lglut -lGLU -lGL -lXi -lXmu -lXext -lX11[Enter]

上のコマンドで main.c をコンパイルしてください。

しかし、これでもまだ次のようなメッセージが出ます。

/tmp/ccOSnV43.o: In function `display':
/tmp/ccOSnV43.o(.text+0xf8): undefined reference to `position'
collect2: ld returned 1 exit status

このページの最初で、 「このプログラムは図形を描くプログラム」だが、 「描く図形はまだ決めていない」と書いていました。 3行目の "position" が見つからないというエラーは、 この描く図形を決める関数です。 この関数をまだ作っていないために、 このエラーが出ています。

そこで、ここではとりあえず main.c をコンパイルのみおこなうことにして、 オブジェクトファイル main.o を作っておきます。 そのためには -c というオプションを使います。

「コンパイラ」 「リンカ」 「オブジェクトファイル」 「ライブラリファイル」 などの意味が分からない人は、 情報処理IIの第1回目の 4.プログラム作成の手順 を復習してください。

問題7-1

cc コマンドに -c オプションを付けて main.c をコンパイルし、 オブジェクトファイル main.o を作成してください。-c オプションを付けると cc コマンドはオブジェクトファイルのリンクを行わず、 コンパイルのみを行います。

% cc -DWIDTH=512 -DHEIGHT=512 -DLOOPS=1000 -c main.c[Enter]

次に、この図形を描く関数を決めましょう。関数 position は main.c の中で次のような形式で呼び出されています。

double t;     /* 角度(ラジアン) */
double p[2];  /* 角度 t における位置、X座標→p[0]、Y座標→p[1] */

position(t, p);

関数 position は、引数 t で与えられた角度 t(0≦t≦2π)から、 何らかの方法で XY 平面上のある点の位置を求め、 それを配列変数 p の各要素に代入するように作ります。

では、X 座標に cos(t)、Y 座標に sin(t) を設定する関数 position を、position.c というファイル名で作成してください。

sin や cos もライブラリファイルに入っている関数(ライブラリ関数)です。 これらを使用するときは、ソースプログラム (のこれらを使用している部分より前)に #include <math.h> を忘れずに入れておいてください。

ソースプログラムができたら、 これをコンパイルのみして position.o を作成してください。

position.c は main.c のように LOOPS や WIDTH、HEIGHT などの記号定数を使っていませんから、 これをコンパイルするときには問題7-1で指定した "-DWIDTH=512 -DHEIGHT=512 -DLOOPS=1000" というオプションは不要です。

最後に、二つのオブジェクトファイル main.o、position.o をリンクして実行ファイル a.out を作成し、実行してみてください。 色の付いた円が描かれたでしょうか?

cc コマンドは 引数にオブジェクトファイルを指定すると、 そのファイルはコンパイルせずにリンクのみを行います。また、 リンク時には記号定数の定義のようなコンパイル時のオプションは意味を持ちません。

しかし、リンク時にはライブラリファイルを指定する必要があります。 このプログラムでは OpenGL のライブラリおよび sin、cos などの算術計算ライブラリを使用しています。

OpenGL(と X Windows)のライブラリの指定
-L/usr/X11R6/lib -lglut -lGLU -lGL -lXi -lXmu -lXext -lX11
算術計算ライブラリの指定
-lm

以上の手順において、cc(あるいは CC)コマンドを3回使用しました。 それぞれが引数やオプションを含めてどのようなコマンド行になったかを、 position.c を印刷した用紙の空白部分に書き込んで提出してください。

Makefile

プログラム開発の時には、 コンパイルやリンクを繰り返し行うことになります。 また、上で挙げた例のように、 コンパイル時やリンク時に複雑な引数やオプションを 指定しなければならない場合もあります。

そういうときにいちいち長ったらしいコマンドを打つのは面倒ですし、 他の人がそのプログラムをコンパイルできるように、 コンパイルに必要なオプションや引数をどこかに書き留めておく必要もあります (自分がコンパイルするときでも、時間が経ってしまうと 『あれ、このプログラムどうやってコンパイルするんだっけな?』という ことはよくあります)。

そういうときは、Makefile というファイルにプログラムを作成する時の 規則 (手順)を記述しておけば、make コマンド一発で自動的にその手順を実行してくれます。

Makefile の書式は以下のようなものです。

fileA: fileB fileC
--Tab-->fileB と fileC から fileA を作るコマンド
--Tab-->fileB と fileC から fileA を作るコマンド
--Tab-->...
fileB: fileD
--Tab-->fileD から fileB を作るコマンド
fileC: fileE
--Tab-->fileE から fileC を作るコマンド
--Tab-->...

Makefile ができたら、make コマンドを実行すれば fileA が作成されます。

% make

最初のターゲット以外のターゲットを作成する場合は、 それを make コマンドの引数に指定します。 つぎの場合は fileB と fileC が作成されますが、 fileA は作成されません。

% make fileB fileC

なお、一度 fileA が作成されると、もう一度 make コマンドを実行しても、fileA を再び作成しようとはしません。 make コマンドは関係するファイルの 変更時間 を調べて、ターゲットが、その元になるファイルより 新しいときは、 ターゲットを作り直しません。

prog.c を cc コマンドでコンパイルして a.out を作成する Makefile は次のようになります。

a.out: prog.c
--Tab-->cc prog.c

また以下のようにすれば、 ターゲットのファイル名を a.out 以外のもの (prog) にできます。

prog: prog.c
--Tab-->cc prog.c
--Tab-->mv a.out prog

でも、これは普通、cc コマンドの -o オプションを使って、 次のように書きます。

prog: prog.c
--Tab-->cc prog.c -o prog

2つ以上のソースファイルから実行プログラムを作成する場合も、 上と同様に書くことができます。

prog: prog1.c prog2.c
--Tab-->cc prog1.c prog2.c -o prog

しかしこれだと、例えば prog1.c を修正して prog を作り直す度に、修正されていない prog2.c までコンパイルし直すことになり、時間の無駄が生じます。

そこで、こういう場合は次のように、 プログラム作成の段階を別けて書きます。

prog: prog1.o prog2.o
--Tab-->cc prog1.o prog2.o -o prog
prog1.o: prog1.c
--Tab-->cc -c prog1.c
prog2.o: prog2.c
--Tab-->cc -c prog2.c

cc コマンドは -c というオプションを与えると、 ソースプログラムのコンパイルのみを行い、実行プログラム a.out の代りにオブジェクトファイル (prog1.o, prog2.o) を生成します。

さらに cc コマンドは、オブジェクトファイルに対してはコンパイル を行わずに、リンクのみを行って実行プログラムを作成します。

なおターゲットが完成し、 これ以上ソースプログラムを修正する必要がない場合には、 オブジェクトファイルは不要になります。 そこで、例えば clean という(仮の)ターゲットを使って 次のような規則を追加しておきます (clean というファイルは作成されません)。 なお、このとき mule/Emacs が作成するバックアップファイル (ファイル名の末尾に ~ が付いたファイル)もついでに削除することにします。

prog: prog1.o prog2.o
--Tab-->cc prog1.o prog2.o -o prog
prog1.o: prog1.c
--Tab-->cc -c prog1.c
prog2.o: prog2.c
--Tab-->cc -c prog2.c
clean: 
--Tab-->-rm -f *.o *~

rm コマンドに -f というオプションをつけた場合は、 ファイルが書き込み保護されていてもメッセージを出さずに削除します。 また rm コマンドのすぐ左に - をつけると、rm コマンドが削除に失敗 (ファイルが見つからないとか)しても、make コマンドはそれを無視します(通常 make はターゲットを作成するためのコマンドの実行に失敗すると、 そこで処理を打ち切ってしまいます)。

問題7-2

問題7-1 のプログラムを作成する Makefile を作成して、 いったん make clean を実行したあと、 make を実行してプログラムが再作成されることを確かめてください。 その後 Makefile を印刷して提出してください。

問題7-3

問題7-1/7-2 で作成したプログラムは、点 (x,y) を以下のように設定することで円を描きました。

x = cos(t)
y = sin(t)

これらの三角関数の周波数や位相を変化させると、 様々な形の曲線(リサージュ曲線)を描くことができます。

x = cos(A t + B)
y = sin(C t + D)

または、次のように複数の三角関数を合成しても、 様々な形の曲線(円トロコイド曲線)を描くことができます (スピログラフ作成 を参照のこと)。

x = B cos(A t) + D cos(C t)
y = B sin(A t) + D sin(C t)

問題7-1 で作成した関数 position を上記のいずれかの形に変更し、 A, B, C, D の4つの定数をマクロ定数で与えるようにしてください。

position.c をコンパイルする際に A, B, C, D を与えることによって、 一つの position.c から次の4つのオブジェクトファイルを作成できるよう、 Makefile を修正してください。

ABCDオブジェクトファイル名
10.210.8position1.o
20.731.2position2.o
31.221.2position3.o
50.531.0position4.o
表1

上のオブジェクトファイルの一つ一つと main.o をリンクして、 4つの実行プログラム prog1, prog2, prog3, prog4 を同時に作成するよう Makefile を修正してください。

複数のターゲットを同時に作成するためには、 Makefile の先頭に、 例えば次のような仮のターゲットを作成する規則を追加します。 この規則にはターゲットを作成するコマンドを記述しません。

all: prog1 prog2 prog3 prog4

こうすると、all というターゲットを作成するためには prog1〜prog4 が必要ということになり、先にこれらが作成されます。 その後 all を作成することになりますが、 all を作成するコマンドを書いていないために all は作成されないまま make は終了します。

作成した Makefile を印刷して提出してください。

条件コンパイル

C 言語(のプリプロセッサ)には、 「条件コンパイル」という機能があります。 これを使えば、マクロ定数の値などを条件にして、 プログラムの一部分をコンパイルしたりしなかったりできます。

#ifdef DEBUG
void debug(char *message)
{
  fprintf(stderr, "DEBUG: %s\n", message);
}
#endif

上の例では、DEBUG という記号定数が定義されているときのみ、 debug() という関数がコンパイルされます。 DEBUG が定義されていないときは、 この関数定義はコンパイルされません(無いものとして扱われます)。

void printmessage(void)
{
#if defined(DEBUG) && LEVEL >= 5
  fprintf(stderr, "レベルは5以上\n");
#else
  fprintf(stderr, "レベルは5未満\n");
#endif
}

上の例では、記号定数 DEBUG が定義されており、かつ、記号定数 LEVEL の値が5以上なら「レベルは5以上」、 そうでなければ「レベルは5未満」と出力します。

関数 printmessage() の中身を変えることができます。

DEBUG や LEVEL は記号定数なので、 その内容を プログラムの実行時に変更することはでき ません。

問題7-4

問題7-3 の表1の定数を、 次のようにしてプログラム内で宣言することを考えてください。

表1では、一つの記号定数について、4つの値を割り当てますから、 そのままでは宣言が重複してしまいます。 そこで、新しい記号定数 LEVEL を導入することにします。 すなわち、表2のように LEVEL の値にしたがって、 記号定数 A, B の宣言を切り替えるプログラムを作成してください。

そのソースプログラムのファイル名は param.h としてください。

LEVELABCDオブジェクトファイル名
110.210.8position1.o
220.731.2position2.o
331.221.2position3.o
450.531.0position4.o
表2

position.c の先頭部分で、この param.h をヘッダファイルとして読み込むよう、 position.c を修正してください。ヘッダファイルの意味が分からない人は、 情報処理II の 第11回 あたりを復習してください。 あるいは、 ここを参考にしてみてください。

問題7-3 と同様に、今度は記号定数 LEVEL に 1 から 4 の値を設定して、 4つの実行プログラムを作成する Makefile を作成してください。

param.h と Makefile を印刷して提出してください。