● モジュール別開発

巨大なプログラムをひとつのファイルで設計するのは管理が大変である。 その些細な改良のため全体をコンパイルし直すは非効率的である。 さらに、そのプログラムの関数を他のプログラムで利用するために editorでその部分のソースをコピーするのでは、その関数が改良された 場合にその変更を他のプログラムへも施すことは大変面倒である。 これらの理由でプログラムの関数群を独立させてモジュールとして開発 するのである。

■ ヘッダーファイル

ヘッダーファイルとは、 stdio.h や math.h の様に、 多くの関数のprototype宣言やdefine定数の定義がなされている ファイルである。関数の実体の定義は別のファイルで行われるのが 普通であるが、ヘッダーファイル内で関数の実体を定義することもできる。 例として、扇型の面積を求める関数 PieArea() 関数を 自作のヘッダーファイル myheadr.h に定義して、 プログラム main.cc から参照しよう。 教育目的のため、扇型の円に対する割合を global変数 ratio で表すことにする。 myheadr.h と main.cc を下に載せる。
// myheader.h
#define PI 3.141592653

double ratio=1.0;

double PieArea( double radius )
{
  double area;
  area = PI*radius*radius*ratio;
  return( area );
}
// main.cc
#include <stdio.h>
#include "myheader.h"

int main( void )
{
  double r, a;
  r = 1.0;
  ratio = 0.25;
  a = PieArea( r );

  return(0);
}
ヘッダーファイル内の記述の仕方はいつも通りである。この中の関数が stdio.h などの他の関数を使う場合にはこのヘッダーファイル内で #include <stdio.h> としておくほうが良い。 main.cc は myheadr.h を #include "" で取り込む。 main.cc をコンパイルすると myheader.h の内容がここに展開されて適切にコンパイルされる。

これで、関数 PieArea() と global変数 ratio は #include するだけで他のプログラムからも利用できる。 これがもっとも簡単なプログラムの分割例であるが、これだけではまだ モジュールとは呼べない。


■ モジュールへの分割

プログラムを分割して、単独でコンパイルもできる .cc の ファイルをモジュールと呼ぶ。 PieArea() 関数をモジュールとして設計し直そう。 宣言だけで実体の定義のないヘッダーファイル myheader.h と 実体を定義するモジュールファイル myheader.cc を以下に載せる。
// myheader.h
#ifndef __MYHEADER_H_INCLUDE
#define __MYHEADER_H_INCLUDE

extern double ratio;

double PieArea( double radius );

#endif  //__MYHEADER_H_INCLUDE

// myheader.cc
#include "myheader.h"
#define PI 3.141592653
double ratio=1.0;

double PieArea( double radius )
{
  double area;
  area = PI*radius*radius*ratio;
  return( area );
}
ファイルの先端と終端の #ifndef 、 #define と #endif は、コンパイル時にこのヘッダーファイル内の諸宣言が不必要に何度も宣言 されるのを防止する小細工である。

global変数の宣言には extern を冠する。変数 ratio の実体は まだ存在しないが、以後に登場するものとして、コンパイラは作業を続ける。 実体がないので値を代入することはできない。関数の宣言もprototypeのみで あり、これもその実体が以後に定義されるものとしてコンパイラは作業を 続ける。

主モジュールファイル main.cc は先のものと同じである。 コンパイルは次の様に各モジュールごとに行う。

  gcc -c myheader.cc
  gcc -c main.cc
両方のコンパイルで myheader.h が読み込まれ適切な判断が行われる。 -c オプションはコンパイラに実行ファイルを作らせずに オブジェクトファイルと呼ばれる中間ファイルを生成させる。 この例では myheader.o と main.o となる。

次にこのふたつのオブジェクトファイルをリンクしてひとつの 実行ファイルを作る。

  gcc -o main main.o myheader.o
これで出来上がりである。 以後、 main.cc を変更したらそれだけを上の手順でコンパイルして 既にある myheader.o とリンクすれば良い。逆も同じである。 こうしてコンパイル作業を効率化することができる。

■ makefile

コンパイル作業は効率化されたがコンパイルのためのコマンドを打ち込む 手作業が複雑になってしまった。 makeコマンドはこの複雑な作業を自動的に行ってくれる。 makeの動作は makefile という名前のファイルで指定する。 指定する内容は各ファイルのコンパイル、リンクの作業の仕方と、 どのファイルが更新されたらどの作業を行うべきかを判断するための ファイル間の依存関係である。 例にそって makefile の記述の仕方を理解しよう。
all: main

myheader.o: myheader.c myheader.h
        gcc -c myheader.c

main.o: main.c myheader.h
        gcc -c main.c

main: main.o myheader.o
        gcc -o main main.o myheader.o
gcc の行の行頭はタブであることに気を付けよう。 1行目の all: に続く main はこの makefile が 生成すべき最終ターゲットが main であることを示す。 3行目の myheader.o: とそれに続く2つのファイル名は myheader.o がこの2つのファイルの内容を汲んで生成されることを 示し、この2つのファイルのどちらかが更新されていたら次の行の コマンドを実行する。以下同様である。 結局、この記述によりmakeは変更箇所だけを自動的に コンパイルしてくれるのである。

こうして巨大なプログラムもモジュールごとに作成され、 最後にmakeでひとつの実行ファイルとなるのである。


■ FORTRANライブラリとのリンク

世の中には FORTRANで作成されコンパイルされた優れたライブラリが 数多くある。それらのライブラリの関数をC++言語のプログラムから 利用することができる。その方法を紹介する。

▲ 変数の型の読み換え

FORTRANの型のうちC言語の基本型に相当するものは以下の通りである。
FORTRAN C言語
Character char
Integer*2 short
Integer long
Logical long
Real float
Double Precision double
C言語の基本型にない次の型もある。
FORTRAN C言語
Character*16 16文字の文字配列
Complex struct{ float real, imag; }
Double Complex struct{ double real, imag; }
本書では FORTRANの Integer はC言語の int に相当する。 FORTRANの Double Complex は本書やC++標準クラスライブラリの Complex 型に相当する。 FORTRANの配列変数 Integer a(10) のindexは 1から10までであるが 気を付けなくて良い 多次元配列の次元の順番はC言語のそれと逆であり、 FORTRANの Integer a(10,20) はC言語の a[20][10] に相当することには気を付ける。 つまり行列の行と列が入れ替わるのである。

▲ サブルーチンの読み換え

FORTRANのサブルーチン名のC言語への読み換えは、その名を小文字にして 最後に _ (underscore)を付ける。 FORTRANではサブルーチンへの引数はすべてアドレス渡しである。 C言語側からはアドレスか参照で渡す。 文字列型の変数を引数に取る関数は文字列配列の長さを long 型で引数の最後に追加する。 以上の読み換えでサブルーチンのprototypeを記述する。 C++言語の場合は extern "C" { } でprototypeを囲む (C++では関数名には引数の型を表す記号列が自動的に付加される。 これにより引数の異なる同じ名前の関数を定義できるのだが、FORTRANとの リンクの場合には余計である。その機能を停止させるのがこの extern "C" 宣言である。)。

例えば次のFORTRANのNAGライブラリのサブルーチンがある。

Subroutine C06EAF(X, N, IFAIL)
Integer          N, IFAIL
Double Precision X(N)
これをC言語またはC++で利用するには、ますprototype宣言を それぞれ次の様にする。
void c06eaf_( double x[], int *n, int *ifail );            // C  
extern "C" void c06eaf_( double x[], int& n, int& ifail ); // C++
そして呼び出しは以下の通りである。
c06eaf_( x, &n, &ifail );                                  // C  
c06eaf_( x,  n,  ifail );                                  // C++

▲ 関数の読み換え

関数の戻り値がC言語の基本型ならば、戻り値の受け取りは 普通に行える。prototype宣言はサブルーチンと同様である。 関数の戻り値がC言語の基本型でない複素数型ならば、戻り値を受け取るべき 複素数型の変数を関数の第1引数にアドレスもしくは参照で渡し、以降に 通常の引数を並べる。 また、戻り値が基本型では無い文字列型ならば、戻り値を受け取るべき 文字列配列のポインタ変数を関数の第1引数に渡し、文字列配列の長さを long 型で第2引数に渡し、以降に通常の引数を並べる。 prototype宣言は戻り値が void で引数が上記の通り変更される。

例えば次のFORTRANのNAGライブラリの関数がある。

Character*30 Function X05ABF (ITIME)
Integer      ITIME(7)
prototype宣言と呼び出しはは以下の通りである。
void x05abf_( char str[], int len, int n[] );              // C  
extern "C" void x05abf_( char str[], int len, int n[] );   // C++

char str[31];
x05abf_( str, 30, n );                            // both C & C++
str[30]='\0';
FORTRANが返す文字列にはNULL終端子( '\0' )が付かないので C言語側で補うことに注意しよう。

▲ 入出力の準備と片付け

FORTRANの関数が入出力を行う場合には、C言語プログラムの最初で 関数 void f_init(void) を実行し、 最後に関数 void f_exit(void) を実行しなくてはならない。 もちろんこの関数のprototype宣言も必要である。

▲ リンク

リンクの作業はgccで行うのだが、 FORTRAN特有のライブラリを幾つかリンクするように指定する。 例えばFORTRAN製のライブラリまたはオブジェクトファイルが fobj.o なら次の様にしてコンパイルとリンクを行う。
  gcc -o main main.cc fobj.o -lF77 -lI77
リンクすべきライブラリは環境によってこれとは異なるかもしれない。

  • 次へ
  • 目次
    Copyright(C) by Naoki Watanabe. Oct 21st, 1995.
    渡辺尚貴 naoki@cms.phys.s.u-tokyo.ac.jp